Repository: arkenfox/TZP Branch: master Commit: 885e0c0b5379 Files: 111 Total size: 2.7 MB Directory structure: gitextract_z_fl_468/ ├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── css/ │ ├── index.css │ ├── media.css │ ├── screen_size.css │ └── window_size.css ├── index.html ├── js/ │ ├── audio.js │ ├── canvas.js │ ├── codecs.js │ ├── css.js │ ├── devices.js │ ├── elements.js │ ├── fonts.js │ ├── generic.js │ ├── globals.js │ ├── iframes.js │ ├── misc.js │ ├── prototypeLies.js │ ├── region.js │ ├── screen.js │ ├── storage.js │ ├── storage_service_worker.js │ ├── storage_shared_worker.js │ ├── user.js │ ├── webgl.js │ ├── worker_agent.js │ ├── worker_service_agent.js │ └── worker_shared_agent.js ├── tests/ │ ├── applang-xslterror.xml │ ├── applang.html │ ├── applang.xml │ ├── bridgemoji.html │ ├── canvasnoise.html │ ├── canvasrfp.html │ ├── canvasspoof.html │ ├── chrome.html │ ├── codecs_can_is.html │ ├── collation.html │ ├── csscolors.html │ ├── dncalendar.html │ ├── dncurrency.html │ ├── dndatetime.html │ ├── dnlanguage.html │ ├── dnregion.html │ ├── dnscript.html │ ├── domrectspoof.html │ ├── domrectspoofratio.html │ ├── dtfcomponents.html │ ├── dtfdatetimestyle.html │ ├── dtfdayperiod.html │ ├── dtflistformat.html │ ├── dtfrelated.html │ ├── dtftimezonename.html │ ├── duration.html │ ├── elementfont.html │ ├── elementforms.html │ ├── elementkeys.html │ ├── elementother.html │ ├── elementother_nocss.html │ ├── engine.html │ ├── engineprop.html │ ├── fontasync.html │ ├── fontdebug.html │ ├── fontdefaults.html │ ├── fontscripts.html │ ├── fontsmac.html │ ├── fontsystem.html │ ├── fontview.html │ ├── functionprops.html │ ├── math.html │ ├── mathdata.html │ ├── mathspoof.html │ ├── newwin.html │ ├── newwinsim.html │ ├── nfcompact.html │ ├── nfcurrency.html │ ├── nfformattoparts.html │ ├── nfnotation.html │ ├── nfsign.html │ ├── nfunit.html │ ├── os.html │ ├── pointerevent.html │ ├── pointertouchevents.html │ ├── pr.html │ ├── prrange.html │ ├── readerview.html │ ├── recursion.html │ ├── recursion_iframe.html │ ├── recursion_worker.js │ ├── resolvedoptions.html │ ├── rtf.html │ ├── sanitizing.html │ ├── screeniframe.html │ ├── screenorientation.html │ ├── scroll.html │ ├── supportedlocales.html │ ├── supportedvalues.html │ ├── testgeneric.js │ ├── testglobals.js │ ├── testindex.css │ ├── timezones.html │ ├── versions.html │ └── windownamea.html ├── tzp.html ├── tzpiframe.html └── xml/ ├── xmlunstyled.xml └── xslterror.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*] charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ * text=auto *.md text *.eps binary *.gif binary *.ico binary *.jpeg binary *.jpg binary *.png binary *.svg binary *.tif binary *.tiff binary *.ttf binary ================================================ FILE: .gitignore ================================================ # Compiled source # ################### *.com *.class *.dll *.exe *.o *.so # Packages # ############ # it's better to unpack these files and commit the raw source # git has its own built in compression methods *.7z *.dmg *.gz *.iso *.jar *.rar *.tar *.zip # Logs and databases # ###################### *.log *.sql *.sqlite # OS files # ############ $RECYCLE.BIN/ *.cab *.lnk *.msi *.msix *.msm *.msp *.stackdump .DS_Store .DS_Store? .Spotlight-V100 .Trashes ._* Thumbs.db [Dd]esktop.ini ehthumbs.db ehthumbs_vista.db ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 arkenfox Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # TorZillaPrint TorZillaPrint (TZP) aims to provide a comprehensive, all-in-one, fingerprinting test suite, nicely broken into suitable sections with relevant information together. Long term, the goal is to collect Gecko only fingerprint data (no PII) for analysis to see how many classifications each metric or section provides. #### 🟥 Fingerprints are ALWAYS loose A fingerprint is just a snapshot of data at any given time, and collected metrics can change for a number or reasons: such as zooming, resizing windows, moving windows, per site settings, etc. Snapshots of fingerprints can still be linked after the fact. Unless you know what is being collected and it's stability, then don't make assumptions. Always treat fingerprints as loose/fuzzy. TZP aims to make sure Tor Browser and RFP are protecting metrics where known, and to dig into more areas of interest to determine equivalency or possible entropy. Non-stable metrics are collected to provide as much information as possible for analysis. #### 🟪 What we do care about: - Gecko - Comparing Tor Browser with Firefox - First party only (for now) - Lowering entropy (or poison pills where appropriate) - Help. We'll take all the help we can get. #### 🟩 What we might care about: - Collecting data via submissions - Expanding to include tests that require third parties #### 🟧 What we don't care about: - non-Gecko - Extensions (except those used in Tor Browser if they affect tests) - Providing entropy figures which requires real world tests with one result per profile #### 🟦 Acknowledgments You know who you are. We don't need to list everyone. You're doing this to make the world a better place - that's your reward. And that's about it, for now. If you want to contribute with your amazing skills - come in and say hello.
version: draftv1.2
date: 10-Feb-2022 ================================================ FILE: css/index.css ================================================ :root{ /* backgrounds */ --bg0: #fbfaf9; /* body */ --bg1: #f09b9b; --bg2: #f0b89b; --bg3: #f0d49b; --bg4: #f0f09b; --bg5: #d4f09b; --bg6: #b8f09b; --bg7: #9bf09b; --bg8: #9bf0b8; --bg9: #9bf0d4; --bg10: #9bf0f0; --bg11: #9bd4f0; --bg12: #9bb8f0; --bg13: #9b9bf0; --bg14: #b89bf0; --bg15: #d49bf0; --bg16: #f09bf0; --bg17: #f09bd4; --bg18: #f09bb8; --bg67: #686868a6; --bg98: #ededed; /* overlaytop */ --bg99: #a4a4a4; /* fingerprint and perf */ --bggood: #bae6aa40; --bgbad: #fb80802e; /* text */ --testh: #000; /* h2 title */ --test0: #0b0b0b; /* body */ --test1: #be0f0f; /* 190, 15, 15 | s218 */ --test2: #c85a1e; /* 200, 90, 30 | s188 */ --test3: #c8870f; /* 200, 135, 15 | s219 */ --test4: #a0a00a; /* 160. 160, 10 | s225 */ --test5: #82b919; /* 130, 185, 25 | s194 */ --test6: #46be0f; /* 70, 190, 15 | s214 */ --test7: #0ab90a; /* 10, 185, 10 | s229 */ --test8: #0aa537; /* 10, 165, 55 | s226 */ --test9: #0a9164; /* 10, 145, 100 | s222 */ --test10: #14aaaa; /* 20, 170, 170 | s201 */ --test11: #1482b4; /* 30, 130, 180 | s204 */ --test12: #3c73e1; /* 60, 115, 225 | s187 */ --test13: #5f5fd7; /* 95, 95, 215 | s152 */ --test14: #8c60e6; /* 140, 95, 230 | s186 */ --test15: #9137be; /* 145, 55, 190 | s141 */ --test16: #d750d7; /* 215, 80, 215 | s160 */ --test17: #b43d8c; /* 180, 60, 140 | s128 */ --test18: #c81f5a; /* 200, 30, 90 | s188 */ --test99: #808080; /* fp + perf */ --testbad: #be0f0f; --testred: #be0f0f; /* not affected by basic mode */ --testweight: bold; /* other */ --txtbasic: #69004f; --txtindicate: #0aa537; /* color change of copy button */ --txtlink: #5079cb; --txtSize: 11px; --txtSizeBigger: 18px; /* json colors */ --jboolean: #058b00; --jkey: #0074e8; --jnull: #5c5c5f; --jstring: #dd00a9; } @media (prefers-color-scheme: light) { /* DARK mode: we only apply it with prefers-light to encourage dark reader extensions to trigger */ :root{ /* backgrounds */ --bg0: #161b22; --bg67: #a2a2a2; --bg98: #161b22; --bg99: #808080; --bggood: none; --bgbad: none; /* text */ --testh: #ffffff; --test0: #b3b3b3; --test1: #dc9d9d; --test2: #dcb29d; --test3: #dcc79d; --test4: #dcdc9d; --test5: #c7dc9d; --test6: #b2dc9d; --test7: #9ddc9d; --test8: #9ddcb2; --test9: #9ddcc7; --test10: #9ddcdc; --test11: #9dc7dc; --test12: #9db2dc; --test13: #9d9ddc; --test14: #b29ddc; --test15: #c79ddc; --test16: #dc9ddc; --test17: #dc9dc7; --test18: #dc9db2; --testbad: #ff8787; --testred: #ff8787; --testweight: normal; /* other */ --txtbasic: #d4c1b3; --txtindicate: white; --txtlink: #9db2dc; /* json colors */ --jstring: #ff7de9; --jboolean: #86de74; --jnull: #939395; --jkey: #75bfff; } } /* so all window measurements are the same: redundant since we contain everything in tzpBody but it can't hurt: note we still measure scrollbars in elements */ html {scrollbar-width: none;} body {background-color: var(--bg0); color: var(--test0);} #tzpBody { position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; overflow-y: auto; } h2 {color: var(--testh); font-size: 14px; text-align: center; margin-top: inherit;} code { background: rgba(142, 142, 145, 0.25) !important; padding: 2px 6px; /* top+bottom | left+ right */ } .s67 { fonmt-weight: bold; text-decoration: underline; } a {color: black; text-decoration: none;} a.blue {color: var(--txtlink); text-decoration: none;} a.return {color: var(--txtlink); text-decoration: none; font-size: 14px; line-height: 1.2em} .no_color {color: var(--test0);} .good {color: var(--test7); background-color: var(--bggood);} .bad {color: var(--testbad); background-color: var(--bgbad);} .red {color: var(--testred); background-color: var(--bgbad);} /* use in basic mode to enforce showing an issue e.g. mismatched metric counts */ .faint {color: var(--test99);} .indicate {color: var(--txtindicate);} .hidden {display: none;} .health, .healthsilent {font-size: 10px;} .smaller {font-size: 11px;} .bigger {font-size: var(--txtSizeBigger);} .offscreen { position: absolute !important; top: -2000% !important; left: 0px !important; } .bold {font-weight: bold;} .normal {font-weight: normal;} .mono {font-family: monospace, "Courier New"; font-size: var(--txtSize);} .strike {text-decoration: line-through;} .spaces {white-space: pre-wrap;} .nospaces {white-space: normal;} .perf {font-family: monospace, "Courier New"; font-size: 12px; white-space: pre-wrap;} .lies {color: var(--test99); text-decoration: underline;} .revert {all: revert;} /* JSON */ .string {color: var(--jstring);} .boolean, .number {color: var(--jboolean);} .null {color: var(--jnull);} .key {color: var(--jkey);} /* buttons */ .btn { display: inline-block; font-size: 12px; font-family: monospace, "Courier New"; padding-left: 4px; padding-right: 6px; cursor: pointer; } .btnright { padding-right: 0px; } .btnb {cursor: pointer;} /* item metrics/counts: dotted, no padding, normal */ .btnc { font-weight: normal; text-decoration: underline; text-decoration-style: dotted; cursor: pointer; } .btns { text-decoration: underline; padding-left: 8px; padding-right: 8px; cursor: pointer; } .btn0, .s0 {color: var(--test0);} .btn1, .s1 {color: var(--test1); cursor: pointer;} .btn2, .s2 {color: var(--test2);} .btn3, .s3 {color: var(--test3);} .btn4, .s4 {color: var(--test4);} .btn5, .s5 {color: var(--test5);} .btn6, .s6 {color: var(--test6);} .btn7, .s7 {color: var(--test7);} .btn8, .s8 {color: var(--test8);} .btn9, .s9 {color: var(--test9);} .btn10, .s10 {color: var(--test10);} .btn11, .s11 {color: var(--test11);} .btn12, .s12 {color: var(--test12);} .btn13, .s13 {color: var(--test13);} .btn14, .s14 {color: var(--test14);} .btn15, .s15 {color: var(--test15);} .btn16, .s16 {color: var(--test16);} .btn17, .s17 {color: var(--test17);} .btn18, .s18 {color: var(--test18);} .btn99, .s99 {color: var(--test99);} .btngood, .sgood {color: var(--test7); background-color: var(--bggood);} .btnbad, .sbad {color: var(--testbad); background-color: var(--bgbad);} .btnred, .sred {color: var(--testred); background-color: var(--bgbad);} .btn-left {float: left; position: relative; left: -15px; top: 0px;} .btn-right {float: right; position: relative; top: 0px; text-align: right;} .btn-right-inset {float: right; position: relative; top: 0px; right: 25%; width: 50%; direction: rtl; } /* tooltips */ .icon {font-size: 10px; font-weight: bold; color: var(--test0); cursor: default;} .ttip {position: relative; display: inline-block; font-weight: normal;} .ttip .ttxt { visibility: hidden; width: 150px; background-color: var(--test0); color: var(--bg0); text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -12px; left: 130%;} .ttip .ttxtb { visibility: hidden; width: 210px; background-color: var(--test0); color: var(--bg0); text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -12px; left: 130%;} .ttip .ttxtx { visibility: hidden; width: 250px; background-color: var(--test0); color: var(--bg0); text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -12px; left: 130%;} .ttip:hover .ttxt, .ttip:hover .ttxtb, .ttip:hover .ttxtx {visibility: visible;} .ttip .ttxt::after, .ttip .ttxtb::after, .ttip .ttxtx::after { content: " "; position: absolute; top: 50%; right: 100%; margin-top: -5px; border-width: 5px; border-style: solid; border-color: transparent var(--test0) transparent transparent;} /* overlay */ #modaloverlay { position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 900; display: none; } #overlay { position: fixed; top: 50%; left: 50%; right: 0; bottom: 0; transform: translate(-50%, -50%); display: none; width: 95%; max-width: 725px; min-width: 225px; height: 85%; border: 2px solid var(--test0); background-color: var(--bg0); box-shadow: 3px 3px 7px black; z-index: 1000; overflow-y: scroll; } #overlaykit { background-image: url('../images/kit-happy.svg'); background-repeat: no-repeat; background-attachment: fixed; height: 40px; transform: scale(-1, 1); filter: sepia(1) hue-rotate(65deg) brightness(0.9) saturate(1.3); z-index: 900; /* works except it moves with scroll position: fixed; bottom: 25px; right: 25px; */ /* works but it's position isn't precise */ position: sticky; top: 92%; width: 97%; } #overlaytop { position: sticky; top: 0; background-color: var(--bg98); border-bottom: 1px solid var(--test0); padding: 18px 25px; z-index: 1000; } #overlaycontent { position: absolute; margin: 15px 25px 10px; padding-bottom: 15px; } #overlaybuttons { float: right; width: fit-content; text-align: right; } /* table nav */ div.nav-title {position: relative; font-weight: bold;} div.nav-down {position: absolute; right: 5px; top: 0px; width: 250px; text-align: right;} div.nav-up {position: absolute; left: 5px; top: 0px; width: 250px; text-align: left;} div.nav-right {position: absolute; right: 5px; top: -2px; width: 0px;} /* tables */ table { width: 98%; min-width: 475px; max-width: 775px; border-collapse: collapse; margin: 0 auto 10px auto; font-size: 12px; } tbody:before {content: "-"; display: block; line-height: 1em; color: transparent;} td {padding-top: 2px; padding-bottom: 3px; padding-left: 10px; vertical-align: top;} th {color: black; font-size: 14px; padding: 3px 0;} table td:first-child { text-align: right; vertical-align: top;} table td.blurb {text-align: center; line-height: 1.5em;} table td.center {text-align: center;} table td.intro {text-align: left; line-height: 1.5em; padding-bottom: 10px; padding-left: 0px;} table td.secthash { text-align: left; vertical-align: top; line-height: 1.2em; padding-bottom: 8px; font-family: monospace, "Courier New"; font-size: 12px; } table td.showhide {text-align: center; padding-top: 7px; padding-bottom: 7px;} tr td.border-bottom { border-bottom: 1px solid var(--test99); border-bottom-style: dashed; } tr td.border-top, div.border-top { border-top: 1px solid var(--test99); border-top-style: dashed; } #tb1 th {background-color: var(--bg1);} #tb2 th {background-color: var(--bg2);} #tb3 th {background-color: var(--bg3);} #tb4 th {background-color: var(--bg4);} #tb5 th {background-color: var(--bg5);} #tb6 th {background-color: var(--bg6);} #tb7 th {background-color: var(--bg7);} #tb8 th {background-color: var(--bg8);} #tb9 th {background-color: var(--bg9);} #tb10 th {background-color: var(--bg10);} #tb11 th {background-color: var(--bg11);} #tb12 th {background-color: var(--bg12);} #tb13 th {background-color: var(--bg13);} #tb14 th {background-color: var(--bg14);} #tb15 th {background-color: var(--bg15);} #tb16 th {background-color: var(--bg16);} #tb17 th {background-color: var(--bg17);} #tb18 th {background-color: var(--bg18);} #tb99 th {background-color: var(--bg99);} #tbfp th {background-color: var(--bg99);} #tbperf th {background-color: var(--bg99);} #tbblock th {background-color: var(--bg99);} .togA, .togP, #btnFS, /* android, perf, fullscreen element */ .togUA, .togUAD, .togAI, .togAW, .togMM, .togCS, .togFS, .togFG, .togL, .togS, .togTP, .togTA, .togTL, .togTT, .togTO {display: none;} #tb1 td:first-child {color: var(--test1); font-weight: var(--testweight)} #tb1 .togS td:first-child {color: var(--test99); font-weight: normal;} /* screen/window/viewport */ #tb2 td:first-child {color: var(--test2); font-weight: var(--testweight)} #tb2 .togUA td:first-child {color: var(--test99); font-weight: normal;} /* useragent */ #tb2 .togUAD td:first-child {color: var(--test99);} /* useragentdata */ #tb2 .togAI td:first-child {color: var(--test99); font-weight: normal;} /* agent iframes */ #tb2 .togAW td:first-child {color: var(--test99); font-weight: normal;} /* agent workers */ #tb3 td:first-child {color: var(--test3); font-weight: var(--testweight)} #tb4 td:first-child {color: var(--test4); font-weight: var(--testweight)} #tb4 .togTT td:first-child {color: var(--test99); font-weight: normal;} /* timezone timezone */ #tb4 .togTL td:first-child {color: var(--test99); font-weight: normal;} /* timezone lastmodified */ #tb4 .togTO td:first-child {color: var(--test99); font-weight: normal;} /* timezone offsets */ #tb5 td:first-child {color: var(--test5); font-weight: var(--testweight)} #tb6 td:first-child {color: var(--test6); font-weight: var(--testweight)} #tb7 td:first-child {color: var(--test7); font-weight: var(--testweight)} #tb8 td:first-child {color: var(--test8); font-weight: var(--testweight)} #tb9 td:first-child {color: var(--test9); font-weight: var(--testweight)} #tb10 td:first-child {color: var(--test10); font-weight: var(--testweight)} #tb11 td:first-child {color: var(--test11); font-weight: var(--testweight)} #tb12 td:first-child {color: var(--test12); font-weight: var(--testweight)} #tb12 .togFS td:first-child {color: var(--test99); font-weight: normal;} /* font sizes */ #tb12 .togFG td:first-child {color: var(--test99); font-weight: normal;} /* font glyphs */ #tb13 td:first-child {color: var(--test13); font-weight: var(--testweight)} #tb14 td:first-child {color: var(--test14); font-weight: var(--testweight)} #tb14 .togCS td:first-child {color: var(--test99); font-weight: normal;} /* computed styles */ #tb14 .togMM td:first-child {color: var(--test99); font-weight: normal;} /* matchmedia + css */ #tb15 td:first-child {color: var(--test15); font-weight: var(--testweight)} #tb16 td:first-child {color: var(--test16); font-weight: var(--testweight)} #tb17 td:first-child {color: var(--test17); font-weight: var(--testweight)} #tb17 .togTA td:first-child {color: var(--test99); font-weight: normal;} /* timing audio */ #tb17 .togTP td:first-child {color: var(--test99); font-weight: normal;} /* timing precision */ #tb18 td:first-child {color: var(--test18); font-weight: var(--testweight)} #tb99 td:first-child {color: var(--test99);} /* index page */ #tbfp td:first-child {color: var(--test99); font-weight: var(--testweight);} #tbperf td:first-child {color: var(--test99);} /* main test */ body.tzpBody::after { content: ""; background: url('chrome://global/skin/onion-pattern.svg'); background-repeat: repeat; opacity: 0.15; top: 0; left: 0; bottom: 0; right: 0; position: fixed; z-index: -1; } #tzpLV { height: 100lvh; width: 100lvw; position: fixed; left: 0; z-index: -6000; } #tzpSV { height: 100svh; width: 100svw; position: fixed; left: 0; z-index: -6000; } #tzpResource { background-image: url("about:logo"), url(""); background-size: auto 100%; background-repeat: no-repeat; background-position: 10px 0px; } #tzpWordmark { background-image: url("chrome://branding/content/about-wordmark.svg"), url(""); background-size: auto 100%; background-repeat: no-repeat; background-position: 10px 0px; } #tzpScroll {width: 100px; overflow-y: scroll;} #tzpRect { position: fixed; top: 0; left: 0; width:100px; height:100px; transform: rotate(45deg); padding: 0px; z-index: -1; } #tzpFS::backdrop { background-color: var(--bg0); /*opacity: 0;*/ } #tzpCalc {width: fit-content;} .tzpCalcContainer {container: calccontainer / inline-size;} .tzpCalcA { width: calc(1px * ((e * 0.06314882636070251 - 0.06699182000011206 / (327510.10546596383 * 101099.74005273856 )) + 0.9363944577053189 / sin( sin( 86911.80023335948 * tan(122224.59393033749) / tan(250486.18265094055) + (169617.27745474092) / pi * 19.00493122072233 - 0.22360279853455722 ) / 50590.01594434995 + tan(( 110958.53977223029) + 109345.15143883083 * 99223.79864865377 + 0.05425928323529661) / 94812.65262083427 * pi) - 0.8964629967231303 * -341499.34226304095 ) ); } /*** FP POCs ***/ .cursive {font-family: cursive;} .emoji {font-family: emoji;} .fangsong {font-family: fangsong;} .fantasy {font-family: fantasy;} .math {font-family: math;} .monospace {font-family: monospace;} .none {font-family: none;} .sans-serif {font-family: sans-serif;} .serif {font-family: serif;} .system-ui {font-family: system-ui;} .ui-monospace {font-family: ui-monospace;} .ui-rounded {font-family: ui-rounded;} .ui-sans-serif {font-family: ui-sans-serif;} .ui-serif {font-family: ui-serif;} .normalized { font-family: none !important; font-size: initial !important; font-style: normal !important; font-variant: normal !important; font-weight: normal !important; line-height: normal !important; text-transform: none !important; text-align: left !important; text-decoration: none !important; text-shadow: none !important; white-space: normal !important; word-break: normal !important; word-spacing: normal !important; } #element-fp { position: fixed; top: 0; left: 0; font-family: none; font-size: initial; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; text-transform: none; text-align: left; text-decoration: none; text-shadow: none; white-space: nowrap; transform: skew(1.787542deg, 3.263901deg); /* domrect */ } #element-fp .unstyled { -moz-appearance: none; -webkit-appearance: none; appearance: none; } #element-fp tbody:before { content: none; line-height: normal; } #element-fp .revert { /* https://developer.mozilla.org/en-US/docs/Web/CSS/all */ all: revert; } .skew {transform: skew(1.787542deg,3.263901deg);} /*** TZP MAIN ***/ @font-face {font-family: "ABR"; src: url("../fonts/AdobeBlankRegular.ttf");} @font-face { font-family: "graphite"; /* src: url("../fonts/GraphiteWidthTest.ttf"); */ src: url(data:font/truetype;base64,AAEAAAAUAQAABABARFNJRwAAAAEAAAFMAAAACEZlYXSAA4EXAAALNAAAABxHbGF0A8sFdwAACjQAAAA2R2xvYwCvAJUAAApsAAAAHk9TLzJRF1vMAAABVAAAAGBTaWxmHoAfnQAACowAAACmU2lsbICBgJQAAAtQAAAAFGNtYXABZABDAAABtAAAAExjdnQgAAAAAAAAAgAAAAGeZnBnbeLCUEIAAAOgAAAAE2dhc3AABwAbAAADtAAAAAxnbHlmnTyrAQAAA8AAAAGoaGVhZCVhj4AAAAVoAAAANmhoZWEG1QJ0AAAFoAAAACRobXR4CSIAvQAABcQAAAAYbG9jYQGKASIAAAXcAAAADm1heHAAcQB4AAAF7AAAACBuYW1lpHI7RgAABgwAAAMPcG9zdFJPeoAAAAkcAAAARXByZXC8yrV/AAAJZAAAAM8AAAABAAAAAAAEAYUBkAAFAAACigJYAAAASwKKAlgAAAFeADIBAwAAAAAEAAAAAAAAAAAAAAMAAAAAAAAAAAAAAABNQUNSAMAAIAAtA6//HwAAA68A4QAAAAEAAAAAAAAAAAAAACAAAAAAAAIAAAADAAAAFAADAAEAAAAUAAQAOAAAAAoACAACAAIAIAArAC0AoP//AAAAIAArAC0AoP///+H/2P/X/2EAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAALHZFILADJUUjYWgYI2hgRC0AAAEAAgAHAAr//wAPAAIAPwAAAbYC+AADAAcAVEAgAQgIQAkCBwQEAQAGBQQDAgUEBwAHBgcBAgEDAAEBAEZ2LzcYAD88LzwQ/TwQ/TwBLzz9PC88/TwAMTABSWixAAhJaGGwQFJYOBE3uQAI/8A4WTMRIRElMxEjPwF3/sf6+gL4/Qg/AnsAAAADACoAZgEwAWwAAwAHAAsAQUAVAQUFQAYAAwAFAgEDAgcABAEAAQRGdi83GAAvPC8Q/TwBLzz9PAAxMAFJaLEEBUloYbBAUlg4ETe5AAX/wDhZEzMRIychFSE1IRUhh01NXQEG/voBBv76AWz++qlNTU0AAAEAKgDCATABDwADAEFAFQEFBUAGAAMABQIBAwIHAAQBAAEERnYvNxgALzwvEP08AS88/TwAMTABSWixBAVJaGGwQFJYOBE3uQAF/8A4WSUhNSEBMP76AQbCTQAAAQAqAMIDJAEPAAMAQUAVAQUFQAYAAwAFAgEDAgcABAEAAQRGdi83GAAvPC8Q/TwBLzz9PAAxMAFJaLEEBUloYbBAUlg4ETe5AAX/wDhZJSE1IQMk/QYC+sJNAAABAAAAAQAAga0g2F8PPPUADwPoAAAAAOF4FG8AAAAA4Xk3NQAqAAADJAL4AAAABwACAAAAAAAAAAEAAAOv/x8AAANOAAAAAAMkAAEAAAAAAAAAAAAAAAAAAAAGAfQAPwEsAAAAAAAAAVoAKgFaACoDTgAqAAAAPgA+AD4AeACmANQAAAABAAAABgAMAAMAAAAAAAIAAgAWAAEAAABkAFQAAAAAAAAAEQDSAAEAAAAAAAEAEwAAAAEAAAAAAAIABwATAAEAAAAAAAQAEwAaAAEAAAAAAAUADQAtAAEAAAAAAAYAEwA6AAEAAAAAAQAABgIrAAMAAQQJAAAAzABNAAMAAQQJAAEAJgEZAAMAAQQJAAIADgE/AAMAAQQJAAMAPAFNAAMAAQQJAAQAJgGJAAMAAQQJAAUAGgGvAAMAAQQJAAYAJgHJAAMAAQQJAAcACAHvAAMAAQQJABAAJgH3AAMAAQQJABEADgIdAAMAAQQJAQAADAIxR3JhcGhpdGUgV2lkdGggVGVzdFJlZ3VsYXJHcmFwaGl0ZSBXaWR0aCBUZXN0VmVyc2lvbiAxLjAwMEdyYXBoaXRlIFdpZHRoIFRlc3QAKABjACkAIABDAG8AcAB5AHIAaQBnAGgAdAAgADIAMAAyADMAIABTAEkATAAgAEkAbgB0AGUAcgBuAGEAdABpAG8AbgBhAGwALAAgADcANQAwADAAIABXAC4AIABDAGEAbQBwACAAVwBpAHMAZABvAG0AIABSAGQALgAsACAARABhAGwAbABhAHMALAAgAFQAWAAgADcANQAyADMANgAgAFUAUwBBACAAKAA5ADcAMgApACAANwAwADgALQA3ADQAOQA1ACAAUgBXAEUARwByAGEAcABoAGkAdABlACAAVwBpAGQAdABoACAAVABlAHMAdABSAGUAZwB1AGwAYQByADEALgAwADAAMAA7AE0AQQBDAFIAOwBHAHIAYQBwAGgAaQB0AGUAIABXAGkAZAB0AGgAIABUAGUAcwB0AEcAcgBhAHAAaABpAHQAZQAgAFcAaQBkAHQAaAAgAFQAZQBzAHQAVgBlAHIAcwBpAG8AbgAgADEALgAwADAAMABHAHIAYQBwAGgAaQB0AGUAIABXAGkAZAB0AGgAIABUAGUAcwB0AE4AbwBuAGUARwByAGEAcABoAGkAdABlACAAVwBpAGQAdABoACAAVABlAHMAdABSAGUAZwB1AGwAYQByTm9OYW1lAE4AbwBOAGEAbQBlAAACAAAAAAAA/5wAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAADAQIADgAQAQMLbm9OYW1lMDAwMDEKaHlwaGVud2lkZQAAAEBHJiYlJSQkIyMiIiEhICAfHx4eHR0cHBsbGhoZGRgYFxcWFhUVFBQTExISEREQEA8PDg4NDQwMCwsKCgkJCAgDAwICAQEAAACNuAH/hUVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoREVoRLMFBEYAK7MHBkYAK7EEBEVoRLEGBkVoRAAAAQAAAQEAHgECAA8ACQEBAB4BAgAeAAUBAgAeAAUBAQAeAQEAHgACAAEAHgACAAEAHgEBAB4AAAABAAAAAAAFAAQACAAOABIAGAAeACIAJgAsADIANgAAAAIAAAABAAAAAAAMAAkAAAAAAQABAf8AAAAAAQIDAAAAAAAAAQAAAAAAAAAABgAAAEwAAACaAAIAAgABAAAAAAAgAAcAAACgAAgAAgACAAoADAAOAAUABAAFAQAAAQAAAAAAlQAAAJUAAACWAAAAAAACAAEAAQABAAEAAQAAAAAABAAEAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAQAAQAAHAAZMQAAAAEAAAABAAAAAAAAAAEAAAAAABiAAAEAAAB//wABAAAAAAAAAAAAAICAgIAAAAAU) format("truetype"); } ================================================ FILE: css/media.css ================================================ /* @supports: https://drafts.csswg.org/css-conditional-5/#at-supports-ext */ /* font-tech */ #cssCOLRv0:after{content:"n/a";} @supports font-tech(color-COLRv0){#cssCOLRv0:after{content:"supported";}} #cssCOLRv1:after{content:"n/a";} @supports font-tech(color-COLRv1){#cssCOLRv1:after{content:"supported";}} #cssOpenType:after{content:"n/a";} @supports font-tech(features-opentype){#cssOpenType:after{content:"supported";}} #cssTrueType:after{content:"n/a";} @supports font-format(TrueType){#cssTrueType:after{content:"supported";}} /* font-format */ #cssWoff2:after{content:"undefined";} @supports font-format(woff2){#cssWoff2:after{content:"supported";}} /* @media */ #cssOm:after{content:"undefined";} @media (-moz-device-orientation:portrait){#cssOm:after{content:"portrait";}} @media (-moz-device-orientation:landscape){#cssOm:after{content:"landscape";}} #cssO:after{content:"undefined";} @media (orientation:portrait){#cssO:after{content:"portrait";}} @media (orientation:landscape){#cssO:after{content:"landscape";}} #cssAR:after{content:"undefined";} @media (aspect-ratio:1/1){#cssAR:after{content:"square";}} @media (min-aspect-ratio:10000/9999){#cssAR:after{content:"landscape";}} @media (max-aspect-ratio:9999/10000){#cssAR:after{content:"portrait";}} #cssDAR:after{content:"undefined";} @media (device-aspect-ratio:1/1){#cssDAR:after{content:"square";}} @media (min-device-aspect-ratio:10000/9999){#cssDAR:after{content:"landscape";}} @media (max-device-aspect-ratio:9999/10000){#cssDAR:after{content:"portrait";}} #cssDM:after{content:"undefined";} @media (display-mode:fullscreen){#cssDM:after{content:"fullscreen";}} @media (display-mode:browser){#cssDM:after{content:"browser";}} @media (display-mode:minimal-ui){#cssDM:after{content:"minimal-ui";}} @media (display-mode:standalone){#cssDM:after{content:"standalone";}} @media (display-mode:picture-in-picture){#cssDM:after{content:"picture-in-picture";}} @media (display-mode:window-controls-overlay){#cssDM:after{content:"window-controls-overlay";}} #cssC:after{content:"n/a";} @media (color:0){#cssC:after{content:"";}} @media (color:1){#cssC:after{content:"1";}} @media (color:2){#cssC:after{content:"2";}} @media (color:3){#cssC:after{content:"3";}} @media (color:4){#cssC:after{content:"4";}} @media (color:5){#cssC:after{content:"5";}} @media (color:6){#cssC:after{content:"6";}} @media (color:7){#cssC:after{content:"7";}} @media (color:8){#cssC:after{content:"8";}} @media (color:9){#cssC:after{content:"9";}} @media (color:10){#cssC:after{content:"10";}} @media (color:11){#cssC:after{content:"11";}} @media (color:12){#cssC:after{content:"12";}} @media (color:13){#cssC:after{content:"13";}} @media (color:14){#cssC:after{content:"14";}} @media (color:15){#cssC:after{content:"15";}} @media (color:16){#cssC:after{content:"16";}} @media (color:17){#cssC:after{content:"17";}} @media (color:18){#cssC:after{content:"18";}} @media (color:19){#cssC:after{content:"19";}} @media (color:20){#cssC:after{content:"20";}} @media (color:21){#cssC:after{content:"21";}} @media (color:22){#cssC:after{content:"22";}} @media (color:23){#cssC:after{content:"23";}} @media (color:24){#cssC:after{content:"24";}} @media (color:25){#cssC:after{content:"";}} /* always return none with a traling space so we can differentiate between this and default none: https://www.w3.org/TR/CSS21/generate.html#content */ #cssH:after{content:"n/a";} @media (hover:hover){#cssH:after{content:"hover";}} @media (hover:none){#cssH:after{content:"none ";}} #cssAH:after{content:"n/a";} @media (any-hover:hover){#cssAH:after{content:"hover";}} @media (any-hover:none){#cssAH:after{content:"none ";}} #cssPRM:after{content:"n/a";} @media (prefers-reduced-motion:no-preference){#cssPRM:after{content:"no-preference";}} @media (prefers-reduced-motion:reduce){#cssPRM:after{content:"reduce";}} #cssP:after{content:"n/a";} @media (pointer:fine){#cssP:after{content:"fine";}} @media (pointer:coarse){#cssP:after{content:"coarse";}} @media (pointer:none){#cssP:after{content:"none ";}} /* any-pointer order matters: https://www.w3.org/TR/mediaqueries-4/#any-input */ #cssAP:before{content:"n/a";} @media (any-pointer:coarse){#cssAP:before{content:"coarse";}} @media (any-pointer:fine){#cssAP:before{content:"fine";}} /* fine over coarse */ @media (any-pointer:none){#cssAP:before{content:"none ";}} #cssAP:after{content:" + n/a";} @media (any-pointer:fine){#cssAP:after{content:" + fine";}} @media (any-pointer:coarse){#cssAP:after{content:" + coarse";}} /* coarse over fine */ @media (any-pointer:none){#cssAP:after{content:" + none ";}} #cssPC:after{content:"n/a";} @media (prefers-contrast:no-preference){#cssPC:after{content:"no-preference";}} @media (prefers-contrast:less){#cssPC:after{content:"less";}} @media (prefers-contrast:more){#cssPC:after{content:"more";}} @media (prefers-contrast:custom){#cssPC:after{content:"custom";}} #cssPCS:after{content:"n/a";} /*note: no-preference: obsolete FF79+: 1643656 */ @media (prefers-color-scheme:light){#cssPCS:after{content:"light";}} @media (prefers-color-scheme:dark){#cssPCS:after{content:"dark";}} #cssFC:after{content:"n/a";} @media (forced-colors:none){#cssFC:after{content:"none ";}} @media (forced-colors:active){#cssFC:after{content:"active";}} #cssDR:after{content:"n/a";} @media (dynamic-range:standard){#cssDR:after{content:"standard";}} @media (dynamic-range:high){#cssDR:after{content:"high";}} #cssVDR:after{content:"n/a";} @media (dynamic-range:standard){#cssVDR:after{content:"standard";}} @media (dynamic-range:high){#cssVDR:after{content:"high";}} #cssCG:after{content:"n/a";} /* "ascending order" see https://drafts.csswg.org/mediaqueries/#color-gamut */ @media (color-gamut:srgb){#cssCG:after{content:"srgb";}} /* narrow */ @media (color-gamut:p3){#cssCG:after{content:"p3";}} /* wider: p3 includes srgb */ @media (color-gamut:rec2020){#cssCG:after{content:"rec2020";}} /* wider: rec2020 includes p3 */ #cssPRT:after{content:"n/a";} @media (prefers-reduced-transparency:no-preference){#cssPRT:after{content:"no-preference";}} @media (prefers-reduced-transparency:reduce){#cssPRT:after{content:"reduce";}} #cssIC:after{content:"n/a";} @media (inverted-colors:none){#cssIC:after{content:"none ";}} @media (inverted-colors:inverted){#cssIC:after{content:"inverted";}} #cssPRD:after{content:"n/a";} @media (prefers-reduced-data:no-preference){#cssPRD:after{content:"no-preference";}} @media (prefers-reduced-data:reduce){#cssPRD:after{content:"reduce";}} #cssDP:after{content:"undefined";} /* use undefined to match navigator */ @media (device-posture:continuous){#cssDP:after{content:"continuous";}} @media (device-posture:folded){#cssDP:after{content:"folded";}} #cssUD:after{content:"n/a";} @media (update:none){#cssUD:after{content:"none";}} /* FF102+: 1422312 || FYI: gecko currently only reports none or fast */ @media (update:slow){#cssUD:after{content:"slow";}} @media (update:fast){#cssUD:after{content:"fast";}} /* https://drafts.csswg.org/mediaqueries-5/#mf-horizontal-viewport-segments */ #cssVS:before{content:"n/a";} @media (horizontal-viewport-segments:1){#cssVS:before{content:"1";}} @media (horizontal-viewport-segments:2){#cssVS:before{content:"2";}} @media (horizontal-viewport-segments:3){#cssVS:before{content:"3";}} @media (horizontal-viewport-segments:4){#cssVS:before{content:"4";}} @media (horizontal-viewport-segments:5){#cssVS:before{content:"5";}} #cssVS:after{content:" x n/a";} @media (vertical-viewport-segments:1){#cssVS:after{content:" x 1";}} @media (vertical-viewport-segments:2){#cssVS:after{content:" x 2";}} @media (vertical-viewport-segments:3){#cssVS:after{content:" x 3";}} @media (vertical-viewport-segments:4){#cssVS:after{content:" x 4";}} @media (vertical-viewport-segments:5){#cssVS:after{content:" x 5";}} /* dpi */ @media (min-resolution:39dpi){#P:before{content:"";}} @media (min-resolution:40dpi){#P:before{content:"40";}} @media (min-resolution:41dpi){#P:before{content:"41";}} @media (min-resolution:42dpi){#P:before{content:"42";}} @media (min-resolution:43dpi){#P:before{content:"43";}} @media (min-resolution:44dpi){#P:before{content:"44";}} @media (min-resolution:45dpi){#P:before{content:"45";}} @media (min-resolution:46dpi){#P:before{content:"46";}} @media (min-resolution:47dpi){#P:before{content:"47";}} @media (min-resolution:48dpi){#P:before{content:"48";}} @media (min-resolution:49dpi){#P:before{content:"49";}} @media (min-resolution:50dpi){#P:before{content:"50";}} @media (min-resolution:51dpi){#P:before{content:"51";}} @media (min-resolution:52dpi){#P:before{content:"52";}} @media (min-resolution:53dpi){#P:before{content:"53";}} @media (min-resolution:54dpi){#P:before{content:"54";}} @media (min-resolution:55dpi){#P:before{content:"55";}} @media (min-resolution:56dpi){#P:before{content:"56";}} @media (min-resolution:57dpi){#P:before{content:"57";}} @media (min-resolution:58dpi){#P:before{content:"58";}} @media (min-resolution:59dpi){#P:before{content:"59";}} @media (min-resolution:60dpi){#P:before{content:"60";}} @media (min-resolution:61dpi){#P:before{content:"61";}} @media (min-resolution:62dpi){#P:before{content:"62";}} @media (min-resolution:63dpi){#P:before{content:"63";}} @media (min-resolution:64dpi){#P:before{content:"64";}} @media (min-resolution:65dpi){#P:before{content:"65";}} @media (min-resolution:66dpi){#P:before{content:"66";}} @media (min-resolution:67dpi){#P:before{content:"67";}} @media (min-resolution:68dpi){#P:before{content:"68";}} @media (min-resolution:69dpi){#P:before{content:"69";}} @media (min-resolution:70dpi){#P:before{content:"70";}} @media (min-resolution:71dpi){#P:before{content:"71";}} @media (min-resolution:72dpi){#P:before{content:"72";}} @media (min-resolution:73dpi){#P:before{content:"73";}} @media (min-resolution:74dpi){#P:before{content:"74";}} @media (min-resolution:75dpi){#P:before{content:"75";}} @media (min-resolution:76dpi){#P:before{content:"76";}} @media (min-resolution:77dpi){#P:before{content:"77";}} @media (min-resolution:78dpi){#P:before{content:"78";}} @media (min-resolution:79dpi){#P:before{content:"79";}} @media (min-resolution:80dpi){#P:before{content:"80";}} @media (min-resolution:81dpi){#P:before{content:"81";}} @media (min-resolution:82dpi){#P:before{content:"82";}} @media (min-resolution:83dpi){#P:before{content:"83";}} @media (min-resolution:84dpi){#P:before{content:"84";}} @media (min-resolution:85dpi){#P:before{content:"85";}} @media (min-resolution:86dpi){#P:before{content:"86";}} @media (min-resolution:87dpi){#P:before{content:"87";}} @media (min-resolution:88dpi){#P:before{content:"88";}} @media (min-resolution:89dpi){#P:before{content:"89";}} @media (min-resolution:90dpi){#P:before{content:"90";}} @media (min-resolution:91dpi){#P:before{content:"91";}} @media (min-resolution:92dpi){#P:before{content:"92";}} @media (min-resolution:93dpi){#P:before{content:"93";}} @media (min-resolution:94dpi){#P:before{content:"94";}} @media (min-resolution:95dpi){#P:before{content:"95";}} @media (min-resolution:96dpi){#P:before{content:"96";}} @media (min-resolution:97dpi){#P:before{content:"97";}} @media (min-resolution:98dpi){#P:before{content:"98";}} @media (min-resolution:99dpi){#P:before{content:"99";}} @media (min-resolution:100dpi){#P:before{content:"100";}} @media (min-resolution:101dpi){#P:before{content:"101";}} @media (min-resolution:102dpi){#P:before{content:"102";}} @media (min-resolution:103dpi){#P:before{content:"103";}} @media (min-resolution:104dpi){#P:before{content:"104";}} @media (min-resolution:105dpi){#P:before{content:"105";}} @media (min-resolution:106dpi){#P:before{content:"106";}} @media (min-resolution:107dpi){#P:before{content:"107";}} @media (min-resolution:108dpi){#P:before{content:"108";}} @media (min-resolution:109dpi){#P:before{content:"109";}} @media (min-resolution:110dpi){#P:before{content:"110";}} @media (min-resolution:111dpi){#P:before{content:"111";}} @media (min-resolution:112dpi){#P:before{content:"112";}} @media (min-resolution:113dpi){#P:before{content:"113";}} @media (min-resolution:114dpi){#P:before{content:"114";}} @media (min-resolution:115dpi){#P:before{content:"115";}} @media (min-resolution:116dpi){#P:before{content:"116";}} @media (min-resolution:117dpi){#P:before{content:"117";}} @media (min-resolution:118dpi){#P:before{content:"118";}} @media (min-resolution:119dpi){#P:before{content:"119";}} @media (min-resolution:120dpi){#P:before{content:"120";}} @media (min-resolution:121dpi){#P:before{content:"121";}} @media (min-resolution:122dpi){#P:before{content:"122";}} @media (min-resolution:123dpi){#P:before{content:"123";}} @media (min-resolution:124dpi){#P:before{content:"124";}} @media (min-resolution:125dpi){#P:before{content:"125";}} @media (min-resolution:126dpi){#P:before{content:"126";}} @media (min-resolution:127dpi){#P:before{content:"127";}} @media (min-resolution:128dpi){#P:before{content:"128";}} @media (min-resolution:129dpi){#P:before{content:"129";}} @media (min-resolution:130dpi){#P:before{content:"130";}} @media (min-resolution:131dpi){#P:before{content:"131";}} @media (min-resolution:132dpi){#P:before{content:"132";}} @media (min-resolution:133dpi){#P:before{content:"133";}} @media (min-resolution:134dpi){#P:before{content:"134";}} @media (min-resolution:135dpi){#P:before{content:"135";}} @media (min-resolution:136dpi){#P:before{content:"136";}} @media (min-resolution:137dpi){#P:before{content:"137";}} @media (min-resolution:138dpi){#P:before{content:"138";}} @media (min-resolution:139dpi){#P:before{content:"139";}} @media (min-resolution:140dpi){#P:before{content:"140";}} @media (min-resolution:141dpi){#P:before{content:"141";}} @media (min-resolution:142dpi){#P:before{content:"142";}} @media (min-resolution:143dpi){#P:before{content:"143";}} @media (min-resolution:144dpi){#P:before{content:"144";}} @media (min-resolution:145dpi){#P:before{content:"145";}} @media (min-resolution:146dpi){#P:before{content:"146";}} @media (min-resolution:147dpi){#P:before{content:"147";}} @media (min-resolution:148dpi){#P:before{content:"148";}} @media (min-resolution:149dpi){#P:before{content:"149";}} @media (min-resolution:150dpi){#P:before{content:"150";}} @media (min-resolution:151dpi){#P:before{content:"151";}} @media (min-resolution:152dpi){#P:before{content:"152";}} @media (min-resolution:153dpi){#P:before{content:"153";}} @media (min-resolution:154dpi){#P:before{content:"154";}} @media (min-resolution:155dpi){#P:before{content:"155";}} @media (min-resolution:156dpi){#P:before{content:"156";}} @media (min-resolution:157dpi){#P:before{content:"157";}} @media (min-resolution:158dpi){#P:before{content:"158";}} @media (min-resolution:159dpi){#P:before{content:"159";}} @media (min-resolution:160dpi){#P:before{content:"160";}} @media (min-resolution:161dpi){#P:before{content:"161";}} @media (min-resolution:162dpi){#P:before{content:"162";}} @media (min-resolution:163dpi){#P:before{content:"163";}} @media (min-resolution:164dpi){#P:before{content:"164";}} @media (min-resolution:165dpi){#P:before{content:"165";}} @media (min-resolution:166dpi){#P:before{content:"166";}} @media (min-resolution:167dpi){#P:before{content:"167";}} @media (min-resolution:168dpi){#P:before{content:"168";}} @media (min-resolution:169dpi){#P:before{content:"169";}} @media (min-resolution:170dpi){#P:before{content:"170";}} @media (min-resolution:171dpi){#P:before{content:"171";}} @media (min-resolution:172dpi){#P:before{content:"172";}} @media (min-resolution:173dpi){#P:before{content:"173";}} @media (min-resolution:174dpi){#P:before{content:"174";}} @media (min-resolution:175dpi){#P:before{content:"175";}} @media (min-resolution:176dpi){#P:before{content:"176";}} @media (min-resolution:177dpi){#P:before{content:"177";}} @media (min-resolution:178dpi){#P:before{content:"178";}} @media (min-resolution:179dpi){#P:before{content:"179";}} @media (min-resolution:180dpi){#P:before{content:"180";}} @media (min-resolution:181dpi){#P:before{content:"181";}} @media (min-resolution:182dpi){#P:before{content:"182";}} @media (min-resolution:183dpi){#P:before{content:"183";}} @media (min-resolution:184dpi){#P:before{content:"184";}} @media (min-resolution:185dpi){#P:before{content:"185";}} @media (min-resolution:186dpi){#P:before{content:"186";}} @media (min-resolution:187dpi){#P:before{content:"187";}} @media (min-resolution:188dpi){#P:before{content:"188";}} @media (min-resolution:189dpi){#P:before{content:"189";}} @media (min-resolution:190dpi){#P:before{content:"190";}} @media (min-resolution:191dpi){#P:before{content:"191";}} @media (min-resolution:192dpi){#P:before{content:"192";}} @media (min-resolution:193dpi){#P:before{content:"193";}} @media (min-resolution:194dpi){#P:before{content:"194";}} @media (min-resolution:195dpi){#P:before{content:"195";}} @media (min-resolution:196dpi){#P:before{content:"196";}} @media (min-resolution:197dpi){#P:before{content:"197";}} @media (min-resolution:198dpi){#P:before{content:"198";}} @media (min-resolution:199dpi){#P:before{content:"199";}} @media (min-resolution:200dpi){#P:before{content:"200";}} @media (min-resolution:201dpi){#P:before{content:"201";}} @media (min-resolution:202dpi){#P:before{content:"202";}} @media (min-resolution:203dpi){#P:before{content:"203";}} @media (min-resolution:204dpi){#P:before{content:"204";}} @media (min-resolution:205dpi){#P:before{content:"205";}} @media (min-resolution:206dpi){#P:before{content:"206";}} @media (min-resolution:207dpi){#P:before{content:"207";}} @media (min-resolution:208dpi){#P:before{content:"208";}} @media (min-resolution:209dpi){#P:before{content:"209";}} @media (min-resolution:210dpi){#P:before{content:"210";}} @media (min-resolution:211dpi){#P:before{content:"211";}} @media (min-resolution:212dpi){#P:before{content:"212";}} @media (min-resolution:213dpi){#P:before{content:"213";}} @media (min-resolution:214dpi){#P:before{content:"214";}} @media (min-resolution:215dpi){#P:before{content:"215";}} @media (min-resolution:216dpi){#P:before{content:"216";}} @media (min-resolution:217dpi){#P:before{content:"217";}} @media (min-resolution:218dpi){#P:before{content:"218";}} @media (min-resolution:219dpi){#P:before{content:"219";}} @media (min-resolution:220dpi){#P:before{content:"220";}} @media (min-resolution:221dpi){#P:before{content:"221";}} @media (min-resolution:222dpi){#P:before{content:"222";}} @media (min-resolution:223dpi){#P:before{content:"223";}} @media (min-resolution:224dpi){#P:before{content:"224";}} @media (min-resolution:225dpi){#P:before{content:"225";}} @media (min-resolution:226dpi){#P:before{content:"226";}} @media (min-resolution:227dpi){#P:before{content:"227";}} @media (min-resolution:228dpi){#P:before{content:"228";}} @media (min-resolution:229dpi){#P:before{content:"229";}} @media (min-resolution:230dpi){#P:before{content:"230";}} @media (min-resolution:231dpi){#P:before{content:"231";}} @media (min-resolution:232dpi){#P:before{content:"232";}} @media (min-resolution:233dpi){#P:before{content:"233";}} @media (min-resolution:234dpi){#P:before{content:"234";}} @media (min-resolution:235dpi){#P:before{content:"235";}} @media (min-resolution:236dpi){#P:before{content:"236";}} @media (min-resolution:237dpi){#P:before{content:"237";}} @media (min-resolution:238dpi){#P:before{content:"238";}} @media (min-resolution:239dpi){#P:before{content:"239";}} @media (min-resolution:240dpi){#P:before{content:"240";}} @media (min-resolution:241dpi){#P:before{content:"241";}} @media (min-resolution:242dpi){#P:before{content:"242";}} @media (min-resolution:243dpi){#P:before{content:"243";}} @media (min-resolution:244dpi){#P:before{content:"244";}} @media (min-resolution:245dpi){#P:before{content:"245";}} @media (min-resolution:246dpi){#P:before{content:"246";}} @media (min-resolution:247dpi){#P:before{content:"247";}} @media (min-resolution:248dpi){#P:before{content:"248";}} @media (min-resolution:249dpi){#P:before{content:"249";}} @media (min-resolution:250dpi){#P:before{content:"250";}} @media (min-resolution:251dpi){#P:before{content:"251";}} @media (min-resolution:252dpi){#P:before{content:"252";}} @media (min-resolution:253dpi){#P:before{content:"253";}} @media (min-resolution:254dpi){#P:before{content:"254";}} @media (min-resolution:255dpi){#P:before{content:"255";}} @media (min-resolution:256dpi){#P:before{content:"256";}} @media (min-resolution:257dpi){#P:before{content:"257";}} @media (min-resolution:258dpi){#P:before{content:"258";}} @media (min-resolution:259dpi){#P:before{content:"259";}} @media (min-resolution:260dpi){#P:before{content:"260";}} @media (min-resolution:261dpi){#P:before{content:"261";}} @media (min-resolution:262dpi){#P:before{content:"262";}} @media (min-resolution:263dpi){#P:before{content:"263";}} @media (min-resolution:264dpi){#P:before{content:"264";}} @media (min-resolution:265dpi){#P:before{content:"265";}} @media (min-resolution:266dpi){#P:before{content:"266";}} @media (min-resolution:267dpi){#P:before{content:"267";}} @media (min-resolution:268dpi){#P:before{content:"268";}} @media (min-resolution:269dpi){#P:before{content:"269";}} @media (min-resolution:270dpi){#P:before{content:"270";}} @media (min-resolution:271dpi){#P:before{content:"271";}} @media (min-resolution:272dpi){#P:before{content:"272";}} @media (min-resolution:273dpi){#P:before{content:"273";}} @media (min-resolution:274dpi){#P:before{content:"274";}} @media (min-resolution:275dpi){#P:before{content:"275";}} @media (min-resolution:276dpi){#P:before{content:"276";}} @media (min-resolution:277dpi){#P:before{content:"277";}} @media (min-resolution:278dpi){#P:before{content:"278";}} @media (min-resolution:279dpi){#P:before{content:"279";}} @media (min-resolution:280dpi){#P:before{content:"280";}} @media (min-resolution:281dpi){#P:before{content:"281";}} @media (min-resolution:282dpi){#P:before{content:"282";}} @media (min-resolution:283dpi){#P:before{content:"283";}} @media (min-resolution:284dpi){#P:before{content:"284";}} @media (min-resolution:285dpi){#P:before{content:"285";}} @media (min-resolution:286dpi){#P:before{content:"286";}} @media (min-resolution:287dpi){#P:before{content:"287";}} @media (min-resolution:288dpi){#P:before{content:"288";}} @media (min-resolution:289dpi){#P:before{content:"289";}} @media (min-resolution:290dpi){#P:before{content:"290";}} @media (min-resolution:291dpi){#P:before{content:"291";}} @media (min-resolution:292dpi){#P:before{content:"292";}} @media (min-resolution:293dpi){#P:before{content:"293";}} @media (min-resolution:294dpi){#P:before{content:"294";}} @media (min-resolution:295dpi){#P:before{content:"295";}} @media (min-resolution:296dpi){#P:before{content:"296";}} @media (min-resolution:297dpi){#P:before{content:"297";}} @media (min-resolution:298dpi){#P:before{content:"298";}} @media (min-resolution:299dpi){#P:before{content:"299";}} @media (min-resolution:300dpi){#P:before{content:"300";}} @media (min-resolution:301dpi){#P:before{content:"301";}} @media (min-resolution:302dpi){#P:before{content:"302";}} @media (min-resolution:303dpi){#P:before{content:"303";}} @media (min-resolution:304dpi){#P:before{content:"304";}} @media (min-resolution:305dpi){#P:before{content:"305";}} @media (min-resolution:306dpi){#P:before{content:"306";}} @media (min-resolution:307dpi){#P:before{content:"307";}} @media (min-resolution:308dpi){#P:before{content:"308";}} @media (min-resolution:309dpi){#P:before{content:"309";}} @media (min-resolution:310dpi){#P:before{content:"310";}} @media (min-resolution:311dpi){#P:before{content:"311";}} @media (min-resolution:312dpi){#P:before{content:"312";}} @media (min-resolution:313dpi){#P:before{content:"313";}} @media (min-resolution:314dpi){#P:before{content:"314";}} @media (min-resolution:315dpi){#P:before{content:"315";}} @media (min-resolution:316dpi){#P:before{content:"316";}} @media (min-resolution:317dpi){#P:before{content:"317";}} @media (min-resolution:318dpi){#P:before{content:"318";}} @media (min-resolution:319dpi){#P:before{content:"319";}} @media (min-resolution:320dpi){#P:before{content:"320";}} @media (min-resolution:321dpi){#P:before{content:"321";}} @media (min-resolution:322dpi){#P:before{content:"322";}} @media (min-resolution:323dpi){#P:before{content:"323";}} @media (min-resolution:324dpi){#P:before{content:"324";}} @media (min-resolution:325dpi){#P:before{content:"325";}} @media (min-resolution:326dpi){#P:before{content:"326";}} @media (min-resolution:327dpi){#P:before{content:"327";}} @media (min-resolution:328dpi){#P:before{content:"328";}} @media (min-resolution:329dpi){#P:before{content:"329";}} @media (min-resolution:330dpi){#P:before{content:"330";}} @media (min-resolution:331dpi){#P:before{content:"331";}} @media (min-resolution:332dpi){#P:before{content:"332";}} @media (min-resolution:333dpi){#P:before{content:"333";}} @media (min-resolution:334dpi){#P:before{content:"334";}} @media (min-resolution:335dpi){#P:before{content:"335";}} @media (min-resolution:336dpi){#P:before{content:"336";}} @media (min-resolution:337dpi){#P:before{content:"337";}} @media (min-resolution:338dpi){#P:before{content:"338";}} @media (min-resolution:339dpi){#P:before{content:"339";}} @media (min-resolution:340dpi){#P:before{content:"340";}} @media (min-resolution:341dpi){#P:before{content:"341";}} @media (min-resolution:342dpi){#P:before{content:"342";}} @media (min-resolution:343dpi){#P:before{content:"343";}} @media (min-resolution:344dpi){#P:before{content:"344";}} @media (min-resolution:345dpi){#P:before{content:"345";}} @media (min-resolution:346dpi){#P:before{content:"346";}} @media (min-resolution:347dpi){#P:before{content:"347";}} @media (min-resolution:348dpi){#P:before{content:"348";}} @media (min-resolution:349dpi){#P:before{content:"349";}} @media (min-resolution:350dpi){#P:before{content:"350";}} @media (min-resolution:351dpi){#P:before{content:"351";}} @media (min-resolution:352dpi){#P:before{content:"352";}} @media (min-resolution:353dpi){#P:before{content:"353";}} @media (min-resolution:354dpi){#P:before{content:"354";}} @media (min-resolution:355dpi){#P:before{content:"355";}} @media (min-resolution:356dpi){#P:before{content:"356";}} @media (min-resolution:357dpi){#P:before{content:"357";}} @media (min-resolution:358dpi){#P:before{content:"358";}} @media (min-resolution:359dpi){#P:before{content:"359";}} @media (min-resolution:360dpi){#P:before{content:"360";}} @media (min-resolution:361dpi){#P:before{content:"361";}} @media (min-resolution:362dpi){#P:before{content:"362";}} @media (min-resolution:363dpi){#P:before{content:"363";}} @media (min-resolution:364dpi){#P:before{content:"364";}} @media (min-resolution:365dpi){#P:before{content:"365";}} @media (min-resolution:366dpi){#P:before{content:"366";}} @media (min-resolution:367dpi){#P:before{content:"367";}} @media (min-resolution:368dpi){#P:before{content:"368";}} @media (min-resolution:369dpi){#P:before{content:"369";}} @media (min-resolution:370dpi){#P:before{content:"370";}} @media (min-resolution:371dpi){#P:before{content:"371";}} @media (min-resolution:372dpi){#P:before{content:"372";}} @media (min-resolution:373dpi){#P:before{content:"373";}} @media (min-resolution:374dpi){#P:before{content:"374";}} @media (min-resolution:375dpi){#P:before{content:"375";}} @media (min-resolution:376dpi){#P:before{content:"376";}} @media (min-resolution:377dpi){#P:before{content:"377";}} @media (min-resolution:378dpi){#P:before{content:"378";}} @media (min-resolution:379dpi){#P:before{content:"379";}} @media (min-resolution:380dpi){#P:before{content:"380";}} @media (min-resolution:381dpi){#P:before{content:"381";}} @media (min-resolution:382dpi){#P:before{content:"382";}} @media (min-resolution:383dpi){#P:before{content:"383";}} @media (min-resolution:384dpi){#P:before{content:"384";}} @media (min-resolution:385dpi){#P:before{content:"385";}} @media (min-resolution:386dpi){#P:before{content:"386";}} @media (min-resolution:387dpi){#P:before{content:"387";}} @media (min-resolution:388dpi){#P:before{content:"388";}} @media (min-resolution:389dpi){#P:before{content:"389";}} @media (min-resolution:390dpi){#P:before{content:"390";}} @media (min-resolution:391dpi){#P:before{content:"391";}} @media (min-resolution:392dpi){#P:before{content:"392";}} @media (min-resolution:393dpi){#P:before{content:"393";}} @media (min-resolution:394dpi){#P:before{content:"394";}} @media (min-resolution:395dpi){#P:before{content:"395";}} @media (min-resolution:396dpi){#P:before{content:"396";}} @media (min-resolution:397dpi){#P:before{content:"397";}} @media (min-resolution:398dpi){#P:before{content:"398";}} @media (min-resolution:399dpi){#P:before{content:"399";}} @media (min-resolution:400dpi){#P:before{content:"400";}} @media (min-resolution:401dpi){#P:before{content:"";}} ================================================ FILE: css/screen_size.css ================================================ @media (min-device-width:399px){#S:before{content:"";}} @media (min-device-width:400px){#S:before{content:"400";}} @media (min-device-width:401px){#S:before{content:"401";}} @media (min-device-width:402px){#S:before{content:"402";}} @media (min-device-width:403px){#S:before{content:"403";}} @media (min-device-width:404px){#S:before{content:"404";}} @media (min-device-width:405px){#S:before{content:"405";}} @media (min-device-width:406px){#S:before{content:"406";}} @media (min-device-width:407px){#S:before{content:"407";}} @media (min-device-width:408px){#S:before{content:"408";}} @media (min-device-width:409px){#S:before{content:"409";}} @media (min-device-width:410px){#S:before{content:"410";}} @media (min-device-width:411px){#S:before{content:"411";}} @media (min-device-width:412px){#S:before{content:"412";}} @media (min-device-width:413px){#S:before{content:"413";}} @media (min-device-width:414px){#S:before{content:"414";}} @media (min-device-width:415px){#S:before{content:"415";}} @media (min-device-width:416px){#S:before{content:"416";}} @media (min-device-width:417px){#S:before{content:"417";}} @media (min-device-width:418px){#S:before{content:"418";}} @media (min-device-width:419px){#S:before{content:"419";}} @media (min-device-width:420px){#S:before{content:"420";}} @media (min-device-width:421px){#S:before{content:"421";}} @media (min-device-width:422px){#S:before{content:"422";}} @media (min-device-width:423px){#S:before{content:"423";}} @media (min-device-width:424px){#S:before{content:"424";}} @media (min-device-width:425px){#S:before{content:"425";}} @media (min-device-width:426px){#S:before{content:"426";}} @media (min-device-width:427px){#S:before{content:"427";}} @media (min-device-width:428px){#S:before{content:"428";}} @media (min-device-width:429px){#S:before{content:"429";}} @media (min-device-width:430px){#S:before{content:"430";}} @media (min-device-width:431px){#S:before{content:"431";}} @media (min-device-width:432px){#S:before{content:"432";}} @media (min-device-width:433px){#S:before{content:"433";}} @media (min-device-width:434px){#S:before{content:"434";}} @media (min-device-width:435px){#S:before{content:"435";}} @media (min-device-width:436px){#S:before{content:"436";}} @media (min-device-width:437px){#S:before{content:"437";}} @media (min-device-width:438px){#S:before{content:"438";}} @media (min-device-width:439px){#S:before{content:"439";}} @media (min-device-width:440px){#S:before{content:"440";}} @media (min-device-width:441px){#S:before{content:"441";}} @media (min-device-width:442px){#S:before{content:"442";}} @media (min-device-width:443px){#S:before{content:"443";}} @media (min-device-width:444px){#S:before{content:"444";}} @media (min-device-width:445px){#S:before{content:"445";}} @media (min-device-width:446px){#S:before{content:"446";}} @media (min-device-width:447px){#S:before{content:"447";}} @media (min-device-width:448px){#S:before{content:"448";}} @media (min-device-width:449px){#S:before{content:"449";}} @media (min-device-width:450px){#S:before{content:"450";}} @media (min-device-width:451px){#S:before{content:"451";}} @media (min-device-width:452px){#S:before{content:"452";}} @media (min-device-width:453px){#S:before{content:"453";}} @media (min-device-width:454px){#S:before{content:"454";}} @media (min-device-width:455px){#S:before{content:"455";}} @media (min-device-width:456px){#S:before{content:"456";}} @media (min-device-width:457px){#S:before{content:"457";}} @media (min-device-width:458px){#S:before{content:"458";}} @media (min-device-width:459px){#S:before{content:"459";}} @media (min-device-width:460px){#S:before{content:"460";}} @media (min-device-width:461px){#S:before{content:"461";}} @media (min-device-width:462px){#S:before{content:"462";}} @media (min-device-width:463px){#S:before{content:"463";}} @media (min-device-width:464px){#S:before{content:"464";}} @media (min-device-width:465px){#S:before{content:"465";}} @media (min-device-width:466px){#S:before{content:"466";}} @media (min-device-width:467px){#S:before{content:"467";}} @media (min-device-width:468px){#S:before{content:"468";}} @media (min-device-width:469px){#S:before{content:"469";}} @media (min-device-width:470px){#S:before{content:"470";}} @media (min-device-width:471px){#S:before{content:"471";}} @media (min-device-width:472px){#S:before{content:"472";}} @media (min-device-width:473px){#S:before{content:"473";}} @media (min-device-width:474px){#S:before{content:"474";}} @media (min-device-width:475px){#S:before{content:"475";}} @media (min-device-width:476px){#S:before{content:"476";}} @media (min-device-width:477px){#S:before{content:"477";}} @media (min-device-width:478px){#S:before{content:"478";}} @media (min-device-width:479px){#S:before{content:"479";}} @media (min-device-width:480px){#S:before{content:"480";}} @media (min-device-width:481px){#S:before{content:"481";}} @media (min-device-width:482px){#S:before{content:"482";}} @media (min-device-width:483px){#S:before{content:"483";}} @media (min-device-width:484px){#S:before{content:"484";}} @media (min-device-width:485px){#S:before{content:"485";}} @media (min-device-width:486px){#S:before{content:"486";}} @media (min-device-width:487px){#S:before{content:"487";}} @media (min-device-width:488px){#S:before{content:"488";}} @media (min-device-width:489px){#S:before{content:"489";}} @media (min-device-width:490px){#S:before{content:"490";}} @media (min-device-width:491px){#S:before{content:"491";}} @media (min-device-width:492px){#S:before{content:"492";}} @media (min-device-width:493px){#S:before{content:"493";}} @media (min-device-width:494px){#S:before{content:"494";}} @media (min-device-width:495px){#S:before{content:"495";}} @media (min-device-width:496px){#S:before{content:"496";}} @media (min-device-width:497px){#S:before{content:"497";}} @media (min-device-width:498px){#S:before{content:"498";}} @media (min-device-width:499px){#S:before{content:"499";}} @media (min-device-width:500px){#S:before{content:"500";}} @media (min-device-width:501px){#S:before{content:"501";}} @media (min-device-width:502px){#S:before{content:"502";}} @media (min-device-width:503px){#S:before{content:"503";}} @media (min-device-width:504px){#S:before{content:"504";}} @media (min-device-width:505px){#S:before{content:"505";}} @media (min-device-width:506px){#S:before{content:"506";}} @media (min-device-width:507px){#S:before{content:"507";}} @media (min-device-width:508px){#S:before{content:"508";}} @media (min-device-width:509px){#S:before{content:"509";}} @media (min-device-width:510px){#S:before{content:"510";}} @media (min-device-width:511px){#S:before{content:"511";}} @media (min-device-width:512px){#S:before{content:"512";}} @media (min-device-width:513px){#S:before{content:"513";}} @media (min-device-width:514px){#S:before{content:"514";}} @media (min-device-width:515px){#S:before{content:"515";}} @media (min-device-width:516px){#S:before{content:"516";}} @media (min-device-width:517px){#S:before{content:"517";}} @media (min-device-width:518px){#S:before{content:"518";}} @media (min-device-width:519px){#S:before{content:"519";}} @media (min-device-width:520px){#S:before{content:"520";}} @media (min-device-width:521px){#S:before{content:"521";}} @media (min-device-width:522px){#S:before{content:"522";}} @media (min-device-width:523px){#S:before{content:"523";}} @media (min-device-width:524px){#S:before{content:"524";}} @media (min-device-width:525px){#S:before{content:"525";}} @media (min-device-width:526px){#S:before{content:"526";}} @media (min-device-width:527px){#S:before{content:"527";}} @media (min-device-width:528px){#S:before{content:"528";}} @media (min-device-width:529px){#S:before{content:"529";}} @media (min-device-width:530px){#S:before{content:"530";}} @media (min-device-width:531px){#S:before{content:"531";}} @media (min-device-width:532px){#S:before{content:"532";}} @media (min-device-width:533px){#S:before{content:"533";}} @media (min-device-width:534px){#S:before{content:"534";}} @media (min-device-width:535px){#S:before{content:"535";}} @media (min-device-width:536px){#S:before{content:"536";}} @media (min-device-width:537px){#S:before{content:"537";}} @media (min-device-width:538px){#S:before{content:"538";}} @media (min-device-width:539px){#S:before{content:"539";}} @media (min-device-width:540px){#S:before{content:"540";}} @media (min-device-width:541px){#S:before{content:"541";}} @media (min-device-width:542px){#S:before{content:"542";}} @media (min-device-width:543px){#S:before{content:"543";}} @media (min-device-width:544px){#S:before{content:"544";}} @media (min-device-width:545px){#S:before{content:"545";}} @media (min-device-width:546px){#S:before{content:"546";}} @media (min-device-width:547px){#S:before{content:"547";}} @media (min-device-width:548px){#S:before{content:"548";}} @media (min-device-width:549px){#S:before{content:"549";}} @media (min-device-width:550px){#S:before{content:"550";}} @media (min-device-width:551px){#S:before{content:"551";}} @media (min-device-width:552px){#S:before{content:"552";}} @media (min-device-width:553px){#S:before{content:"553";}} @media (min-device-width:554px){#S:before{content:"554";}} @media (min-device-width:555px){#S:before{content:"555";}} @media (min-device-width:556px){#S:before{content:"556";}} @media (min-device-width:557px){#S:before{content:"557";}} @media (min-device-width:558px){#S:before{content:"558";}} @media (min-device-width:559px){#S:before{content:"559";}} @media (min-device-width:560px){#S:before{content:"560";}} @media (min-device-width:561px){#S:before{content:"561";}} @media (min-device-width:562px){#S:before{content:"562";}} @media (min-device-width:563px){#S:before{content:"563";}} @media (min-device-width:564px){#S:before{content:"564";}} @media (min-device-width:565px){#S:before{content:"565";}} @media (min-device-width:566px){#S:before{content:"566";}} @media (min-device-width:567px){#S:before{content:"567";}} @media (min-device-width:568px){#S:before{content:"568";}} @media (min-device-width:569px){#S:before{content:"569";}} @media (min-device-width:570px){#S:before{content:"570";}} @media (min-device-width:571px){#S:before{content:"571";}} @media (min-device-width:572px){#S:before{content:"572";}} @media (min-device-width:573px){#S:before{content:"573";}} @media (min-device-width:574px){#S:before{content:"574";}} @media (min-device-width:575px){#S:before{content:"575";}} @media (min-device-width:576px){#S:before{content:"576";}} @media (min-device-width:577px){#S:before{content:"577";}} @media (min-device-width:578px){#S:before{content:"578";}} @media (min-device-width:579px){#S:before{content:"579";}} @media (min-device-width:580px){#S:before{content:"580";}} @media (min-device-width:581px){#S:before{content:"581";}} @media (min-device-width:582px){#S:before{content:"582";}} @media (min-device-width:583px){#S:before{content:"583";}} @media (min-device-width:584px){#S:before{content:"584";}} @media (min-device-width:585px){#S:before{content:"585";}} @media (min-device-width:586px){#S:before{content:"586";}} @media (min-device-width:587px){#S:before{content:"587";}} @media (min-device-width:588px){#S:before{content:"588";}} @media (min-device-width:589px){#S:before{content:"589";}} @media (min-device-width:590px){#S:before{content:"590";}} @media (min-device-width:591px){#S:before{content:"591";}} @media (min-device-width:592px){#S:before{content:"592";}} @media (min-device-width:593px){#S:before{content:"593";}} @media (min-device-width:594px){#S:before{content:"594";}} @media (min-device-width:595px){#S:before{content:"595";}} @media (min-device-width:596px){#S:before{content:"596";}} @media (min-device-width:597px){#S:before{content:"597";}} @media (min-device-width:598px){#S:before{content:"598";}} @media (min-device-width:599px){#S:before{content:"599";}} @media (min-device-width:600px){#S:before{content:"600";}} @media (min-device-width:601px){#S:before{content:"601";}} @media (min-device-width:602px){#S:before{content:"602";}} @media (min-device-width:603px){#S:before{content:"603";}} @media (min-device-width:604px){#S:before{content:"604";}} @media (min-device-width:605px){#S:before{content:"605";}} @media (min-device-width:606px){#S:before{content:"606";}} @media (min-device-width:607px){#S:before{content:"607";}} @media (min-device-width:608px){#S:before{content:"608";}} @media (min-device-width:609px){#S:before{content:"609";}} @media (min-device-width:610px){#S:before{content:"610";}} @media (min-device-width:611px){#S:before{content:"611";}} @media (min-device-width:612px){#S:before{content:"612";}} @media (min-device-width:613px){#S:before{content:"613";}} @media (min-device-width:614px){#S:before{content:"614";}} @media (min-device-width:615px){#S:before{content:"615";}} @media (min-device-width:616px){#S:before{content:"616";}} @media (min-device-width:617px){#S:before{content:"617";}} @media (min-device-width:618px){#S:before{content:"618";}} @media (min-device-width:619px){#S:before{content:"619";}} @media (min-device-width:620px){#S:before{content:"620";}} @media (min-device-width:621px){#S:before{content:"621";}} @media (min-device-width:622px){#S:before{content:"622";}} @media (min-device-width:623px){#S:before{content:"623";}} @media (min-device-width:624px){#S:before{content:"624";}} @media (min-device-width:625px){#S:before{content:"625";}} @media (min-device-width:626px){#S:before{content:"626";}} @media (min-device-width:627px){#S:before{content:"627";}} @media (min-device-width:628px){#S:before{content:"628";}} @media (min-device-width:629px){#S:before{content:"629";}} @media (min-device-width:630px){#S:before{content:"630";}} @media (min-device-width:631px){#S:before{content:"631";}} @media (min-device-width:632px){#S:before{content:"632";}} @media (min-device-width:633px){#S:before{content:"633";}} @media (min-device-width:634px){#S:before{content:"634";}} @media (min-device-width:635px){#S:before{content:"635";}} @media (min-device-width:636px){#S:before{content:"636";}} @media (min-device-width:637px){#S:before{content:"637";}} @media (min-device-width:638px){#S:before{content:"638";}} @media (min-device-width:639px){#S:before{content:"639";}} @media (min-device-width:640px){#S:before{content:"640";}} @media (min-device-width:641px){#S:before{content:"641";}} @media (min-device-width:642px){#S:before{content:"642";}} @media (min-device-width:643px){#S:before{content:"643";}} @media (min-device-width:644px){#S:before{content:"644";}} @media (min-device-width:645px){#S:before{content:"645";}} @media (min-device-width:646px){#S:before{content:"646";}} @media (min-device-width:647px){#S:before{content:"647";}} @media (min-device-width:648px){#S:before{content:"648";}} @media (min-device-width:649px){#S:before{content:"649";}} @media (min-device-width:650px){#S:before{content:"650";}} @media (min-device-width:651px){#S:before{content:"651";}} @media (min-device-width:652px){#S:before{content:"652";}} @media (min-device-width:653px){#S:before{content:"653";}} @media (min-device-width:654px){#S:before{content:"654";}} @media (min-device-width:655px){#S:before{content:"655";}} @media (min-device-width:656px){#S:before{content:"656";}} @media (min-device-width:657px){#S:before{content:"657";}} @media (min-device-width:658px){#S:before{content:"658";}} @media (min-device-width:659px){#S:before{content:"659";}} @media (min-device-width:660px){#S:before{content:"660";}} @media (min-device-width:661px){#S:before{content:"661";}} @media (min-device-width:662px){#S:before{content:"662";}} @media (min-device-width:663px){#S:before{content:"663";}} @media (min-device-width:664px){#S:before{content:"664";}} @media (min-device-width:665px){#S:before{content:"665";}} @media (min-device-width:666px){#S:before{content:"666";}} @media (min-device-width:667px){#S:before{content:"667";}} @media (min-device-width:668px){#S:before{content:"668";}} @media (min-device-width:669px){#S:before{content:"669";}} @media (min-device-width:670px){#S:before{content:"670";}} @media (min-device-width:671px){#S:before{content:"671";}} @media (min-device-width:672px){#S:before{content:"672";}} @media (min-device-width:673px){#S:before{content:"673";}} @media (min-device-width:674px){#S:before{content:"674";}} @media (min-device-width:675px){#S:before{content:"675";}} @media (min-device-width:676px){#S:before{content:"676";}} @media (min-device-width:677px){#S:before{content:"677";}} @media (min-device-width:678px){#S:before{content:"678";}} @media (min-device-width:679px){#S:before{content:"679";}} @media (min-device-width:680px){#S:before{content:"680";}} @media (min-device-width:681px){#S:before{content:"681";}} @media (min-device-width:682px){#S:before{content:"682";}} @media (min-device-width:683px){#S:before{content:"683";}} @media (min-device-width:684px){#S:before{content:"684";}} @media (min-device-width:685px){#S:before{content:"685";}} @media (min-device-width:686px){#S:before{content:"686";}} @media (min-device-width:687px){#S:before{content:"687";}} @media (min-device-width:688px){#S:before{content:"688";}} @media (min-device-width:689px){#S:before{content:"689";}} @media (min-device-width:690px){#S:before{content:"690";}} @media (min-device-width:691px){#S:before{content:"691";}} @media (min-device-width:692px){#S:before{content:"692";}} @media (min-device-width:693px){#S:before{content:"693";}} @media (min-device-width:694px){#S:before{content:"694";}} @media (min-device-width:695px){#S:before{content:"695";}} @media (min-device-width:696px){#S:before{content:"696";}} @media (min-device-width:697px){#S:before{content:"697";}} @media (min-device-width:698px){#S:before{content:"698";}} @media (min-device-width:699px){#S:before{content:"699";}} @media (min-device-width:700px){#S:before{content:"700";}} @media (min-device-width:701px){#S:before{content:"701";}} @media (min-device-width:702px){#S:before{content:"702";}} @media (min-device-width:703px){#S:before{content:"703";}} @media (min-device-width:704px){#S:before{content:"704";}} @media (min-device-width:705px){#S:before{content:"705";}} @media (min-device-width:706px){#S:before{content:"706";}} @media (min-device-width:707px){#S:before{content:"707";}} @media (min-device-width:708px){#S:before{content:"708";}} @media (min-device-width:709px){#S:before{content:"709";}} @media (min-device-width:710px){#S:before{content:"710";}} @media (min-device-width:711px){#S:before{content:"711";}} @media (min-device-width:712px){#S:before{content:"712";}} @media (min-device-width:713px){#S:before{content:"713";}} @media (min-device-width:714px){#S:before{content:"714";}} @media (min-device-width:715px){#S:before{content:"715";}} @media (min-device-width:716px){#S:before{content:"716";}} @media (min-device-width:717px){#S:before{content:"717";}} @media (min-device-width:718px){#S:before{content:"718";}} @media (min-device-width:719px){#S:before{content:"719";}} @media (min-device-width:720px){#S:before{content:"720";}} @media (min-device-width:721px){#S:before{content:"721";}} @media (min-device-width:722px){#S:before{content:"722";}} @media (min-device-width:723px){#S:before{content:"723";}} @media (min-device-width:724px){#S:before{content:"724";}} @media (min-device-width:725px){#S:before{content:"725";}} @media (min-device-width:726px){#S:before{content:"726";}} @media (min-device-width:727px){#S:before{content:"727";}} @media (min-device-width:728px){#S:before{content:"728";}} @media (min-device-width:729px){#S:before{content:"729";}} @media (min-device-width:730px){#S:before{content:"730";}} @media (min-device-width:731px){#S:before{content:"731";}} @media (min-device-width:732px){#S:before{content:"732";}} @media (min-device-width:733px){#S:before{content:"733";}} @media (min-device-width:734px){#S:before{content:"734";}} @media (min-device-width:735px){#S:before{content:"735";}} @media (min-device-width:736px){#S:before{content:"736";}} @media (min-device-width:737px){#S:before{content:"737";}} @media (min-device-width:738px){#S:before{content:"738";}} @media (min-device-width:739px){#S:before{content:"739";}} @media (min-device-width:740px){#S:before{content:"740";}} @media (min-device-width:741px){#S:before{content:"741";}} @media (min-device-width:742px){#S:before{content:"742";}} @media (min-device-width:743px){#S:before{content:"743";}} @media (min-device-width:744px){#S:before{content:"744";}} @media (min-device-width:745px){#S:before{content:"745";}} @media (min-device-width:746px){#S:before{content:"746";}} @media (min-device-width:747px){#S:before{content:"747";}} @media (min-device-width:748px){#S:before{content:"748";}} @media (min-device-width:749px){#S:before{content:"749";}} @media (min-device-width:750px){#S:before{content:"750";}} @media (min-device-width:751px){#S:before{content:"751";}} @media (min-device-width:752px){#S:before{content:"752";}} @media (min-device-width:753px){#S:before{content:"753";}} @media (min-device-width:754px){#S:before{content:"754";}} @media (min-device-width:755px){#S:before{content:"755";}} @media (min-device-width:756px){#S:before{content:"756";}} @media (min-device-width:757px){#S:before{content:"757";}} @media (min-device-width:758px){#S:before{content:"758";}} @media (min-device-width:759px){#S:before{content:"759";}} @media (min-device-width:760px){#S:before{content:"760";}} @media (min-device-width:761px){#S:before{content:"761";}} @media (min-device-width:762px){#S:before{content:"762";}} @media (min-device-width:763px){#S:before{content:"763";}} @media (min-device-width:764px){#S:before{content:"764";}} @media (min-device-width:765px){#S:before{content:"765";}} @media (min-device-width:766px){#S:before{content:"766";}} @media (min-device-width:767px){#S:before{content:"767";}} @media (min-device-width:768px){#S:before{content:"768";}} @media (min-device-width:769px){#S:before{content:"769";}} @media (min-device-width:770px){#S:before{content:"770";}} @media (min-device-width:771px){#S:before{content:"771";}} @media (min-device-width:772px){#S:before{content:"772";}} @media (min-device-width:773px){#S:before{content:"773";}} @media (min-device-width:774px){#S:before{content:"774";}} @media (min-device-width:775px){#S:before{content:"775";}} @media (min-device-width:776px){#S:before{content:"776";}} @media (min-device-width:777px){#S:before{content:"777";}} @media (min-device-width:778px){#S:before{content:"778";}} @media (min-device-width:779px){#S:before{content:"779";}} @media (min-device-width:780px){#S:before{content:"780";}} @media (min-device-width:781px){#S:before{content:"781";}} @media (min-device-width:782px){#S:before{content:"782";}} @media (min-device-width:783px){#S:before{content:"783";}} @media (min-device-width:784px){#S:before{content:"784";}} @media (min-device-width:785px){#S:before{content:"785";}} @media (min-device-width:786px){#S:before{content:"786";}} @media (min-device-width:787px){#S:before{content:"787";}} @media (min-device-width:788px){#S:before{content:"788";}} @media (min-device-width:789px){#S:before{content:"789";}} @media (min-device-width:790px){#S:before{content:"790";}} @media (min-device-width:791px){#S:before{content:"791";}} @media (min-device-width:792px){#S:before{content:"792";}} @media (min-device-width:793px){#S:before{content:"793";}} @media (min-device-width:794px){#S:before{content:"794";}} @media (min-device-width:795px){#S:before{content:"795";}} @media (min-device-width:796px){#S:before{content:"796";}} @media (min-device-width:797px){#S:before{content:"797";}} @media (min-device-width:798px){#S:before{content:"798";}} @media (min-device-width:799px){#S:before{content:"799";}} @media (min-device-width:800px){#S:before{content:"800";}} @media (min-device-width:801px){#S:before{content:"801";}} @media (min-device-width:802px){#S:before{content:"802";}} @media (min-device-width:803px){#S:before{content:"803";}} @media (min-device-width:804px){#S:before{content:"804";}} @media (min-device-width:805px){#S:before{content:"805";}} @media (min-device-width:806px){#S:before{content:"806";}} @media (min-device-width:807px){#S:before{content:"807";}} @media (min-device-width:808px){#S:before{content:"808";}} @media (min-device-width:809px){#S:before{content:"809";}} @media (min-device-width:810px){#S:before{content:"810";}} @media (min-device-width:811px){#S:before{content:"811";}} @media (min-device-width:812px){#S:before{content:"812";}} @media (min-device-width:813px){#S:before{content:"813";}} @media (min-device-width:814px){#S:before{content:"814";}} @media (min-device-width:815px){#S:before{content:"815";}} @media (min-device-width:816px){#S:before{content:"816";}} @media (min-device-width:817px){#S:before{content:"817";}} @media (min-device-width:818px){#S:before{content:"818";}} @media (min-device-width:819px){#S:before{content:"819";}} @media (min-device-width:820px){#S:before{content:"820";}} @media (min-device-width:821px){#S:before{content:"821";}} @media (min-device-width:822px){#S:before{content:"822";}} @media (min-device-width:823px){#S:before{content:"823";}} @media (min-device-width:824px){#S:before{content:"824";}} @media (min-device-width:825px){#S:before{content:"825";}} @media (min-device-width:826px){#S:before{content:"826";}} @media (min-device-width:827px){#S:before{content:"827";}} @media (min-device-width:828px){#S:before{content:"828";}} @media (min-device-width:829px){#S:before{content:"829";}} @media (min-device-width:830px){#S:before{content:"830";}} @media (min-device-width:831px){#S:before{content:"831";}} @media (min-device-width:832px){#S:before{content:"832";}} @media (min-device-width:833px){#S:before{content:"833";}} @media (min-device-width:834px){#S:before{content:"834";}} @media (min-device-width:835px){#S:before{content:"835";}} @media (min-device-width:836px){#S:before{content:"836";}} @media (min-device-width:837px){#S:before{content:"837";}} @media (min-device-width:838px){#S:before{content:"838";}} @media (min-device-width:839px){#S:before{content:"839";}} @media (min-device-width:840px){#S:before{content:"840";}} @media (min-device-width:841px){#S:before{content:"841";}} @media (min-device-width:842px){#S:before{content:"842";}} @media (min-device-width:843px){#S:before{content:"843";}} @media (min-device-width:844px){#S:before{content:"844";}} @media (min-device-width:845px){#S:before{content:"845";}} @media (min-device-width:846px){#S:before{content:"846";}} @media (min-device-width:847px){#S:before{content:"847";}} @media (min-device-width:848px){#S:before{content:"848";}} @media (min-device-width:849px){#S:before{content:"849";}} @media (min-device-width:850px){#S:before{content:"850";}} @media (min-device-width:851px){#S:before{content:"851";}} @media (min-device-width:852px){#S:before{content:"852";}} @media (min-device-width:853px){#S:before{content:"853";}} @media (min-device-width:854px){#S:before{content:"854";}} @media (min-device-width:855px){#S:before{content:"855";}} @media (min-device-width:856px){#S:before{content:"856";}} @media (min-device-width:857px){#S:before{content:"857";}} @media (min-device-width:858px){#S:before{content:"858";}} @media (min-device-width:859px){#S:before{content:"859";}} @media (min-device-width:860px){#S:before{content:"860";}} @media (min-device-width:861px){#S:before{content:"861";}} @media (min-device-width:862px){#S:before{content:"862";}} @media (min-device-width:863px){#S:before{content:"863";}} @media (min-device-width:864px){#S:before{content:"864";}} @media (min-device-width:865px){#S:before{content:"865";}} @media (min-device-width:866px){#S:before{content:"866";}} @media (min-device-width:867px){#S:before{content:"867";}} @media (min-device-width:868px){#S:before{content:"868";}} @media (min-device-width:869px){#S:before{content:"869";}} @media (min-device-width:870px){#S:before{content:"870";}} @media (min-device-width:871px){#S:before{content:"871";}} @media (min-device-width:872px){#S:before{content:"872";}} @media (min-device-width:873px){#S:before{content:"873";}} @media (min-device-width:874px){#S:before{content:"874";}} @media (min-device-width:875px){#S:before{content:"875";}} @media (min-device-width:876px){#S:before{content:"876";}} @media (min-device-width:877px){#S:before{content:"877";}} @media (min-device-width:878px){#S:before{content:"878";}} @media (min-device-width:879px){#S:before{content:"879";}} @media (min-device-width:880px){#S:before{content:"880";}} @media (min-device-width:881px){#S:before{content:"881";}} @media (min-device-width:882px){#S:before{content:"882";}} @media (min-device-width:883px){#S:before{content:"883";}} @media (min-device-width:884px){#S:before{content:"884";}} @media (min-device-width:885px){#S:before{content:"885";}} @media (min-device-width:886px){#S:before{content:"886";}} @media (min-device-width:887px){#S:before{content:"887";}} @media (min-device-width:888px){#S:before{content:"888";}} @media (min-device-width:889px){#S:before{content:"889";}} @media (min-device-width:890px){#S:before{content:"890";}} @media (min-device-width:891px){#S:before{content:"891";}} @media (min-device-width:892px){#S:before{content:"892";}} @media (min-device-width:893px){#S:before{content:"893";}} @media (min-device-width:894px){#S:before{content:"894";}} @media (min-device-width:895px){#S:before{content:"895";}} @media (min-device-width:896px){#S:before{content:"896";}} @media (min-device-width:897px){#S:before{content:"897";}} @media (min-device-width:898px){#S:before{content:"898";}} @media (min-device-width:899px){#S:before{content:"899";}} @media (min-device-width:900px){#S:before{content:"900";}} @media (min-device-width:901px){#S:before{content:"901";}} @media (min-device-width:902px){#S:before{content:"902";}} @media (min-device-width:903px){#S:before{content:"903";}} @media (min-device-width:904px){#S:before{content:"904";}} @media (min-device-width:905px){#S:before{content:"905";}} @media (min-device-width:906px){#S:before{content:"906";}} @media (min-device-width:907px){#S:before{content:"907";}} @media (min-device-width:908px){#S:before{content:"908";}} @media (min-device-width:909px){#S:before{content:"909";}} @media (min-device-width:910px){#S:before{content:"910";}} @media (min-device-width:911px){#S:before{content:"911";}} @media (min-device-width:912px){#S:before{content:"912";}} @media (min-device-width:913px){#S:before{content:"913";}} @media (min-device-width:914px){#S:before{content:"914";}} @media (min-device-width:915px){#S:before{content:"915";}} @media (min-device-width:916px){#S:before{content:"916";}} @media (min-device-width:917px){#S:before{content:"917";}} @media (min-device-width:918px){#S:before{content:"918";}} @media (min-device-width:919px){#S:before{content:"919";}} @media (min-device-width:920px){#S:before{content:"920";}} @media (min-device-width:921px){#S:before{content:"921";}} @media (min-device-width:922px){#S:before{content:"922";}} @media (min-device-width:923px){#S:before{content:"923";}} @media (min-device-width:924px){#S:before{content:"924";}} @media (min-device-width:925px){#S:before{content:"925";}} @media (min-device-width:926px){#S:before{content:"926";}} @media (min-device-width:927px){#S:before{content:"927";}} @media (min-device-width:928px){#S:before{content:"928";}} @media (min-device-width:929px){#S:before{content:"929";}} @media (min-device-width:930px){#S:before{content:"930";}} @media (min-device-width:931px){#S:before{content:"931";}} @media (min-device-width:932px){#S:before{content:"932";}} @media (min-device-width:933px){#S:before{content:"933";}} @media (min-device-width:934px){#S:before{content:"934";}} @media (min-device-width:935px){#S:before{content:"935";}} @media (min-device-width:936px){#S:before{content:"936";}} @media (min-device-width:937px){#S:before{content:"937";}} @media (min-device-width:938px){#S:before{content:"938";}} @media (min-device-width:939px){#S:before{content:"939";}} @media (min-device-width:940px){#S:before{content:"940";}} @media (min-device-width:941px){#S:before{content:"941";}} @media (min-device-width:942px){#S:before{content:"942";}} @media (min-device-width:943px){#S:before{content:"943";}} @media (min-device-width:944px){#S:before{content:"944";}} @media (min-device-width:945px){#S:before{content:"945";}} @media (min-device-width:946px){#S:before{content:"946";}} @media (min-device-width:947px){#S:before{content:"947";}} @media (min-device-width:948px){#S:before{content:"948";}} @media (min-device-width:949px){#S:before{content:"949";}} @media (min-device-width:950px){#S:before{content:"950";}} @media (min-device-width:951px){#S:before{content:"951";}} @media (min-device-width:952px){#S:before{content:"952";}} @media (min-device-width:953px){#S:before{content:"953";}} @media (min-device-width:954px){#S:before{content:"954";}} @media (min-device-width:955px){#S:before{content:"955";}} @media (min-device-width:956px){#S:before{content:"956";}} @media (min-device-width:957px){#S:before{content:"957";}} @media (min-device-width:958px){#S:before{content:"958";}} @media (min-device-width:959px){#S:before{content:"959";}} @media (min-device-width:960px){#S:before{content:"960";}} @media (min-device-width:961px){#S:before{content:"961";}} @media (min-device-width:962px){#S:before{content:"962";}} @media (min-device-width:963px){#S:before{content:"963";}} @media (min-device-width:964px){#S:before{content:"964";}} @media (min-device-width:965px){#S:before{content:"965";}} @media (min-device-width:966px){#S:before{content:"966";}} @media (min-device-width:967px){#S:before{content:"967";}} @media (min-device-width:968px){#S:before{content:"968";}} @media (min-device-width:969px){#S:before{content:"969";}} @media (min-device-width:970px){#S:before{content:"970";}} @media (min-device-width:971px){#S:before{content:"971";}} @media (min-device-width:972px){#S:before{content:"972";}} @media (min-device-width:973px){#S:before{content:"973";}} @media (min-device-width:974px){#S:before{content:"974";}} @media (min-device-width:975px){#S:before{content:"975";}} @media (min-device-width:976px){#S:before{content:"976";}} @media (min-device-width:977px){#S:before{content:"977";}} @media (min-device-width:978px){#S:before{content:"978";}} @media (min-device-width:979px){#S:before{content:"979";}} @media (min-device-width:980px){#S:before{content:"980";}} @media (min-device-width:981px){#S:before{content:"981";}} @media (min-device-width:982px){#S:before{content:"982";}} @media (min-device-width:983px){#S:before{content:"983";}} @media (min-device-width:984px){#S:before{content:"984";}} @media (min-device-width:985px){#S:before{content:"985";}} @media (min-device-width:986px){#S:before{content:"986";}} @media (min-device-width:987px){#S:before{content:"987";}} @media (min-device-width:988px){#S:before{content:"988";}} @media (min-device-width:989px){#S:before{content:"989";}} @media (min-device-width:990px){#S:before{content:"990";}} @media (min-device-width:991px){#S:before{content:"991";}} @media (min-device-width:992px){#S:before{content:"992";}} @media (min-device-width:993px){#S:before{content:"993";}} @media (min-device-width:994px){#S:before{content:"994";}} @media (min-device-width:995px){#S:before{content:"995";}} @media (min-device-width:996px){#S:before{content:"996";}} @media (min-device-width:997px){#S:before{content:"997";}} @media (min-device-width:998px){#S:before{content:"998";}} @media (min-device-width:999px){#S:before{content:"999";}} @media (min-device-width:1000px){#S:before{content:"1000";}} @media (min-device-width:1001px){#S:before{content:"1001";}} @media (min-device-width:1002px){#S:before{content:"1002";}} @media (min-device-width:1003px){#S:before{content:"1003";}} @media (min-device-width:1004px){#S:before{content:"1004";}} @media (min-device-width:1005px){#S:before{content:"1005";}} @media (min-device-width:1006px){#S:before{content:"1006";}} @media (min-device-width:1007px){#S:before{content:"1007";}} @media (min-device-width:1008px){#S:before{content:"1008";}} @media (min-device-width:1009px){#S:before{content:"1009";}} @media (min-device-width:1010px){#S:before{content:"1010";}} @media (min-device-width:1011px){#S:before{content:"1011";}} @media (min-device-width:1012px){#S:before{content:"1012";}} @media (min-device-width:1013px){#S:before{content:"1013";}} @media (min-device-width:1014px){#S:before{content:"1014";}} @media (min-device-width:1015px){#S:before{content:"1015";}} @media (min-device-width:1016px){#S:before{content:"1016";}} @media (min-device-width:1017px){#S:before{content:"1017";}} @media (min-device-width:1018px){#S:before{content:"1018";}} @media (min-device-width:1019px){#S:before{content:"1019";}} @media (min-device-width:1020px){#S:before{content:"1020";}} @media (min-device-width:1021px){#S:before{content:"1021";}} @media (min-device-width:1022px){#S:before{content:"1022";}} @media (min-device-width:1023px){#S:before{content:"1023";}} @media (min-device-width:1024px){#S:before{content:"1024";}} @media (min-device-width:1025px){#S:before{content:"1025";}} @media (min-device-width:1026px){#S:before{content:"1026";}} @media (min-device-width:1027px){#S:before{content:"1027";}} @media (min-device-width:1028px){#S:before{content:"1028";}} @media (min-device-width:1029px){#S:before{content:"1029";}} @media (min-device-width:1030px){#S:before{content:"1030";}} @media (min-device-width:1031px){#S:before{content:"1031";}} @media (min-device-width:1032px){#S:before{content:"1032";}} @media (min-device-width:1033px){#S:before{content:"1033";}} @media (min-device-width:1034px){#S:before{content:"1034";}} @media (min-device-width:1035px){#S:before{content:"1035";}} @media (min-device-width:1036px){#S:before{content:"1036";}} @media (min-device-width:1037px){#S:before{content:"1037";}} @media (min-device-width:1038px){#S:before{content:"1038";}} @media (min-device-width:1039px){#S:before{content:"1039";}} @media (min-device-width:1040px){#S:before{content:"1040";}} @media (min-device-width:1041px){#S:before{content:"1041";}} @media (min-device-width:1042px){#S:before{content:"1042";}} @media (min-device-width:1043px){#S:before{content:"1043";}} @media (min-device-width:1044px){#S:before{content:"1044";}} @media (min-device-width:1045px){#S:before{content:"1045";}} @media (min-device-width:1046px){#S:before{content:"1046";}} @media (min-device-width:1047px){#S:before{content:"1047";}} @media (min-device-width:1048px){#S:before{content:"1048";}} @media (min-device-width:1049px){#S:before{content:"1049";}} @media (min-device-width:1050px){#S:before{content:"1050";}} @media (min-device-width:1051px){#S:before{content:"1051";}} @media (min-device-width:1052px){#S:before{content:"1052";}} @media (min-device-width:1053px){#S:before{content:"1053";}} @media (min-device-width:1054px){#S:before{content:"1054";}} @media (min-device-width:1055px){#S:before{content:"1055";}} @media (min-device-width:1056px){#S:before{content:"1056";}} @media (min-device-width:1057px){#S:before{content:"1057";}} @media (min-device-width:1058px){#S:before{content:"1058";}} @media (min-device-width:1059px){#S:before{content:"1059";}} @media (min-device-width:1060px){#S:before{content:"1060";}} @media (min-device-width:1061px){#S:before{content:"1061";}} @media (min-device-width:1062px){#S:before{content:"1062";}} @media (min-device-width:1063px){#S:before{content:"1063";}} @media (min-device-width:1064px){#S:before{content:"1064";}} @media (min-device-width:1065px){#S:before{content:"1065";}} @media (min-device-width:1066px){#S:before{content:"1066";}} @media (min-device-width:1067px){#S:before{content:"1067";}} @media (min-device-width:1068px){#S:before{content:"1068";}} @media (min-device-width:1069px){#S:before{content:"1069";}} @media (min-device-width:1070px){#S:before{content:"1070";}} @media (min-device-width:1071px){#S:before{content:"1071";}} @media (min-device-width:1072px){#S:before{content:"1072";}} @media (min-device-width:1073px){#S:before{content:"1073";}} @media (min-device-width:1074px){#S:before{content:"1074";}} @media (min-device-width:1075px){#S:before{content:"1075";}} @media (min-device-width:1076px){#S:before{content:"1076";}} @media (min-device-width:1077px){#S:before{content:"1077";}} @media (min-device-width:1078px){#S:before{content:"1078";}} @media (min-device-width:1079px){#S:before{content:"1079";}} @media (min-device-width:1080px){#S:before{content:"1080";}} @media (min-device-width:1081px){#S:before{content:"1081";}} @media (min-device-width:1082px){#S:before{content:"1082";}} @media (min-device-width:1083px){#S:before{content:"1083";}} @media (min-device-width:1084px){#S:before{content:"1084";}} @media (min-device-width:1085px){#S:before{content:"1085";}} @media (min-device-width:1086px){#S:before{content:"1086";}} @media (min-device-width:1087px){#S:before{content:"1087";}} @media (min-device-width:1088px){#S:before{content:"1088";}} @media (min-device-width:1089px){#S:before{content:"1089";}} @media (min-device-width:1090px){#S:before{content:"1090";}} @media (min-device-width:1091px){#S:before{content:"1091";}} @media (min-device-width:1092px){#S:before{content:"1092";}} @media (min-device-width:1093px){#S:before{content:"1093";}} @media (min-device-width:1094px){#S:before{content:"1094";}} @media (min-device-width:1095px){#S:before{content:"1095";}} @media (min-device-width:1096px){#S:before{content:"1096";}} @media (min-device-width:1097px){#S:before{content:"1097";}} @media (min-device-width:1098px){#S:before{content:"1098";}} @media (min-device-width:1099px){#S:before{content:"1099";}} @media (min-device-width:1100px){#S:before{content:"1100";}} @media (min-device-width:1101px){#S:before{content:"1101";}} @media (min-device-width:1102px){#S:before{content:"1102";}} @media (min-device-width:1103px){#S:before{content:"1103";}} @media (min-device-width:1104px){#S:before{content:"1104";}} @media (min-device-width:1105px){#S:before{content:"1105";}} @media (min-device-width:1106px){#S:before{content:"1106";}} @media (min-device-width:1107px){#S:before{content:"1107";}} @media (min-device-width:1108px){#S:before{content:"1108";}} @media (min-device-width:1109px){#S:before{content:"1109";}} @media (min-device-width:1110px){#S:before{content:"1110";}} @media (min-device-width:1111px){#S:before{content:"1111";}} @media (min-device-width:1112px){#S:before{content:"1112";}} @media (min-device-width:1113px){#S:before{content:"1113";}} @media (min-device-width:1114px){#S:before{content:"1114";}} @media (min-device-width:1115px){#S:before{content:"1115";}} @media (min-device-width:1116px){#S:before{content:"1116";}} @media (min-device-width:1117px){#S:before{content:"1117";}} @media (min-device-width:1118px){#S:before{content:"1118";}} @media (min-device-width:1119px){#S:before{content:"1119";}} @media (min-device-width:1120px){#S:before{content:"1120";}} @media (min-device-width:1121px){#S:before{content:"1121";}} @media (min-device-width:1122px){#S:before{content:"1122";}} @media (min-device-width:1123px){#S:before{content:"1123";}} @media (min-device-width:1124px){#S:before{content:"1124";}} @media (min-device-width:1125px){#S:before{content:"1125";}} @media (min-device-width:1126px){#S:before{content:"1126";}} @media (min-device-width:1127px){#S:before{content:"1127";}} @media (min-device-width:1128px){#S:before{content:"1128";}} @media (min-device-width:1129px){#S:before{content:"1129";}} @media (min-device-width:1130px){#S:before{content:"1130";}} @media (min-device-width:1131px){#S:before{content:"1131";}} @media (min-device-width:1132px){#S:before{content:"1132";}} @media (min-device-width:1133px){#S:before{content:"1133";}} @media (min-device-width:1134px){#S:before{content:"1134";}} @media (min-device-width:1135px){#S:before{content:"1135";}} @media (min-device-width:1136px){#S:before{content:"1136";}} @media (min-device-width:1137px){#S:before{content:"1137";}} @media (min-device-width:1138px){#S:before{content:"1138";}} @media (min-device-width:1139px){#S:before{content:"1139";}} @media (min-device-width:1140px){#S:before{content:"1140";}} @media (min-device-width:1141px){#S:before{content:"1141";}} @media (min-device-width:1142px){#S:before{content:"1142";}} @media (min-device-width:1143px){#S:before{content:"1143";}} @media (min-device-width:1144px){#S:before{content:"1144";}} @media (min-device-width:1145px){#S:before{content:"1145";}} @media (min-device-width:1146px){#S:before{content:"1146";}} @media (min-device-width:1147px){#S:before{content:"1147";}} @media (min-device-width:1148px){#S:before{content:"1148";}} @media (min-device-width:1149px){#S:before{content:"1149";}} @media (min-device-width:1150px){#S:before{content:"1150";}} @media (min-device-width:1151px){#S:before{content:"1151";}} @media (min-device-width:1152px){#S:before{content:"1152";}} @media (min-device-width:1153px){#S:before{content:"1153";}} @media (min-device-width:1154px){#S:before{content:"1154";}} @media (min-device-width:1155px){#S:before{content:"1155";}} @media (min-device-width:1156px){#S:before{content:"1156";}} @media (min-device-width:1157px){#S:before{content:"1157";}} @media (min-device-width:1158px){#S:before{content:"1158";}} @media (min-device-width:1159px){#S:before{content:"1159";}} @media (min-device-width:1160px){#S:before{content:"1160";}} @media (min-device-width:1161px){#S:before{content:"1161";}} @media (min-device-width:1162px){#S:before{content:"1162";}} @media (min-device-width:1163px){#S:before{content:"1163";}} @media (min-device-width:1164px){#S:before{content:"1164";}} @media (min-device-width:1165px){#S:before{content:"1165";}} @media (min-device-width:1166px){#S:before{content:"1166";}} @media (min-device-width:1167px){#S:before{content:"1167";}} @media (min-device-width:1168px){#S:before{content:"1168";}} @media (min-device-width:1169px){#S:before{content:"1169";}} @media (min-device-width:1170px){#S:before{content:"1170";}} @media (min-device-width:1171px){#S:before{content:"1171";}} @media (min-device-width:1172px){#S:before{content:"1172";}} @media (min-device-width:1173px){#S:before{content:"1173";}} @media (min-device-width:1174px){#S:before{content:"1174";}} @media (min-device-width:1175px){#S:before{content:"1175";}} @media (min-device-width:1176px){#S:before{content:"1176";}} @media (min-device-width:1177px){#S:before{content:"1177";}} @media (min-device-width:1178px){#S:before{content:"1178";}} @media (min-device-width:1179px){#S:before{content:"1179";}} @media (min-device-width:1180px){#S:before{content:"1180";}} @media (min-device-width:1181px){#S:before{content:"1181";}} @media (min-device-width:1182px){#S:before{content:"1182";}} @media (min-device-width:1183px){#S:before{content:"1183";}} @media (min-device-width:1184px){#S:before{content:"1184";}} @media (min-device-width:1185px){#S:before{content:"1185";}} @media (min-device-width:1186px){#S:before{content:"1186";}} @media (min-device-width:1187px){#S:before{content:"1187";}} @media (min-device-width:1188px){#S:before{content:"1188";}} @media (min-device-width:1189px){#S:before{content:"1189";}} @media (min-device-width:1190px){#S:before{content:"1190";}} @media (min-device-width:1191px){#S:before{content:"1191";}} @media (min-device-width:1192px){#S:before{content:"1192";}} @media (min-device-width:1193px){#S:before{content:"1193";}} @media (min-device-width:1194px){#S:before{content:"1194";}} @media (min-device-width:1195px){#S:before{content:"1195";}} @media (min-device-width:1196px){#S:before{content:"1196";}} @media (min-device-width:1197px){#S:before{content:"1197";}} @media (min-device-width:1198px){#S:before{content:"1198";}} @media (min-device-width:1199px){#S:before{content:"1199";}} @media (min-device-width:1200px){#S:before{content:"1200";}} @media (min-device-width:1201px){#S:before{content:"1201";}} @media (min-device-width:1202px){#S:before{content:"1202";}} @media (min-device-width:1203px){#S:before{content:"1203";}} @media (min-device-width:1204px){#S:before{content:"1204";}} @media (min-device-width:1205px){#S:before{content:"1205";}} @media (min-device-width:1206px){#S:before{content:"1206";}} @media (min-device-width:1207px){#S:before{content:"1207";}} @media (min-device-width:1208px){#S:before{content:"1208";}} @media (min-device-width:1209px){#S:before{content:"1209";}} @media (min-device-width:1210px){#S:before{content:"1210";}} @media (min-device-width:1211px){#S:before{content:"1211";}} @media (min-device-width:1212px){#S:before{content:"1212";}} @media (min-device-width:1213px){#S:before{content:"1213";}} @media (min-device-width:1214px){#S:before{content:"1214";}} @media (min-device-width:1215px){#S:before{content:"1215";}} @media (min-device-width:1216px){#S:before{content:"1216";}} @media (min-device-width:1217px){#S:before{content:"1217";}} @media (min-device-width:1218px){#S:before{content:"1218";}} @media (min-device-width:1219px){#S:before{content:"1219";}} @media (min-device-width:1220px){#S:before{content:"1220";}} @media (min-device-width:1221px){#S:before{content:"1221";}} @media (min-device-width:1222px){#S:before{content:"1222";}} @media (min-device-width:1223px){#S:before{content:"1223";}} @media (min-device-width:1224px){#S:before{content:"1224";}} @media (min-device-width:1225px){#S:before{content:"1225";}} @media (min-device-width:1226px){#S:before{content:"1226";}} @media (min-device-width:1227px){#S:before{content:"1227";}} @media (min-device-width:1228px){#S:before{content:"1228";}} @media (min-device-width:1229px){#S:before{content:"1229";}} @media (min-device-width:1230px){#S:before{content:"1230";}} @media (min-device-width:1231px){#S:before{content:"1231";}} @media (min-device-width:1232px){#S:before{content:"1232";}} @media (min-device-width:1233px){#S:before{content:"1233";}} @media (min-device-width:1234px){#S:before{content:"1234";}} @media (min-device-width:1235px){#S:before{content:"1235";}} @media (min-device-width:1236px){#S:before{content:"1236";}} @media (min-device-width:1237px){#S:before{content:"1237";}} @media (min-device-width:1238px){#S:before{content:"1238";}} @media (min-device-width:1239px){#S:before{content:"1239";}} @media (min-device-width:1240px){#S:before{content:"1240";}} @media (min-device-width:1241px){#S:before{content:"1241";}} @media (min-device-width:1242px){#S:before{content:"1242";}} @media (min-device-width:1243px){#S:before{content:"1243";}} @media (min-device-width:1244px){#S:before{content:"1244";}} @media (min-device-width:1245px){#S:before{content:"1245";}} @media (min-device-width:1246px){#S:before{content:"1246";}} @media (min-device-width:1247px){#S:before{content:"1247";}} @media (min-device-width:1248px){#S:before{content:"1248";}} @media (min-device-width:1249px){#S:before{content:"1249";}} @media (min-device-width:1250px){#S:before{content:"1250";}} @media (min-device-width:1251px){#S:before{content:"1251";}} @media (min-device-width:1252px){#S:before{content:"1252";}} @media (min-device-width:1253px){#S:before{content:"1253";}} @media (min-device-width:1254px){#S:before{content:"1254";}} @media (min-device-width:1255px){#S:before{content:"1255";}} @media (min-device-width:1256px){#S:before{content:"1256";}} @media (min-device-width:1257px){#S:before{content:"1257";}} @media (min-device-width:1258px){#S:before{content:"1258";}} @media (min-device-width:1259px){#S:before{content:"1259";}} @media (min-device-width:1260px){#S:before{content:"1260";}} @media (min-device-width:1261px){#S:before{content:"1261";}} @media (min-device-width:1262px){#S:before{content:"1262";}} @media (min-device-width:1263px){#S:before{content:"1263";}} @media (min-device-width:1264px){#S:before{content:"1264";}} @media (min-device-width:1265px){#S:before{content:"1265";}} @media (min-device-width:1266px){#S:before{content:"1266";}} @media (min-device-width:1267px){#S:before{content:"1267";}} @media (min-device-width:1268px){#S:before{content:"1268";}} @media (min-device-width:1269px){#S:before{content:"1269";}} @media (min-device-width:1270px){#S:before{content:"1270";}} @media (min-device-width:1271px){#S:before{content:"1271";}} @media (min-device-width:1272px){#S:before{content:"1272";}} @media (min-device-width:1273px){#S:before{content:"1273";}} @media (min-device-width:1274px){#S:before{content:"1274";}} @media (min-device-width:1275px){#S:before{content:"1275";}} @media (min-device-width:1276px){#S:before{content:"1276";}} @media (min-device-width:1277px){#S:before{content:"1277";}} @media (min-device-width:1278px){#S:before{content:"1278";}} @media (min-device-width:1279px){#S:before{content:"1279";}} @media (min-device-width:1280px){#S:before{content:"1280";}} @media (min-device-width:1281px){#S:before{content:"1281";}} @media (min-device-width:1282px){#S:before{content:"1282";}} @media (min-device-width:1283px){#S:before{content:"1283";}} @media (min-device-width:1284px){#S:before{content:"1284";}} @media (min-device-width:1285px){#S:before{content:"1285";}} @media (min-device-width:1286px){#S:before{content:"1286";}} @media (min-device-width:1287px){#S:before{content:"1287";}} @media (min-device-width:1288px){#S:before{content:"1288";}} @media (min-device-width:1289px){#S:before{content:"1289";}} @media (min-device-width:1290px){#S:before{content:"1290";}} @media (min-device-width:1291px){#S:before{content:"1291";}} @media (min-device-width:1292px){#S:before{content:"1292";}} @media (min-device-width:1293px){#S:before{content:"1293";}} @media (min-device-width:1294px){#S:before{content:"1294";}} @media (min-device-width:1295px){#S:before{content:"1295";}} @media (min-device-width:1296px){#S:before{content:"1296";}} @media (min-device-width:1297px){#S:before{content:"1297";}} @media (min-device-width:1298px){#S:before{content:"1298";}} @media (min-device-width:1299px){#S:before{content:"1299";}} @media (min-device-width:1300px){#S:before{content:"1300";}} @media (min-device-width:1301px){#S:before{content:"1301";}} @media (min-device-width:1302px){#S:before{content:"1302";}} @media (min-device-width:1303px){#S:before{content:"1303";}} @media (min-device-width:1304px){#S:before{content:"1304";}} @media (min-device-width:1305px){#S:before{content:"1305";}} @media (min-device-width:1306px){#S:before{content:"1306";}} @media (min-device-width:1307px){#S:before{content:"1307";}} @media (min-device-width:1308px){#S:before{content:"1308";}} @media (min-device-width:1309px){#S:before{content:"1309";}} @media (min-device-width:1310px){#S:before{content:"1310";}} @media (min-device-width:1311px){#S:before{content:"1311";}} @media (min-device-width:1312px){#S:before{content:"1312";}} @media (min-device-width:1313px){#S:before{content:"1313";}} @media (min-device-width:1314px){#S:before{content:"1314";}} @media (min-device-width:1315px){#S:before{content:"1315";}} @media (min-device-width:1316px){#S:before{content:"1316";}} @media (min-device-width:1317px){#S:before{content:"1317";}} @media (min-device-width:1318px){#S:before{content:"1318";}} @media (min-device-width:1319px){#S:before{content:"1319";}} @media (min-device-width:1320px){#S:before{content:"1320";}} @media (min-device-width:1321px){#S:before{content:"1321";}} @media (min-device-width:1322px){#S:before{content:"1322";}} @media (min-device-width:1323px){#S:before{content:"1323";}} @media (min-device-width:1324px){#S:before{content:"1324";}} @media (min-device-width:1325px){#S:before{content:"1325";}} @media (min-device-width:1326px){#S:before{content:"1326";}} @media (min-device-width:1327px){#S:before{content:"1327";}} @media (min-device-width:1328px){#S:before{content:"1328";}} @media (min-device-width:1329px){#S:before{content:"1329";}} @media (min-device-width:1330px){#S:before{content:"1330";}} @media (min-device-width:1331px){#S:before{content:"1331";}} @media (min-device-width:1332px){#S:before{content:"1332";}} @media (min-device-width:1333px){#S:before{content:"1333";}} @media (min-device-width:1334px){#S:before{content:"1334";}} @media (min-device-width:1335px){#S:before{content:"1335";}} @media (min-device-width:1336px){#S:before{content:"1336";}} @media (min-device-width:1337px){#S:before{content:"1337";}} @media (min-device-width:1338px){#S:before{content:"1338";}} @media (min-device-width:1339px){#S:before{content:"1339";}} @media (min-device-width:1340px){#S:before{content:"1340";}} @media (min-device-width:1341px){#S:before{content:"1341";}} @media (min-device-width:1342px){#S:before{content:"1342";}} @media (min-device-width:1343px){#S:before{content:"1343";}} @media (min-device-width:1344px){#S:before{content:"1344";}} @media (min-device-width:1345px){#S:before{content:"1345";}} @media (min-device-width:1346px){#S:before{content:"1346";}} @media (min-device-width:1347px){#S:before{content:"1347";}} @media (min-device-width:1348px){#S:before{content:"1348";}} @media (min-device-width:1349px){#S:before{content:"1349";}} @media (min-device-width:1350px){#S:before{content:"1350";}} @media (min-device-width:1351px){#S:before{content:"1351";}} @media (min-device-width:1352px){#S:before{content:"1352";}} @media (min-device-width:1353px){#S:before{content:"1353";}} @media (min-device-width:1354px){#S:before{content:"1354";}} @media (min-device-width:1355px){#S:before{content:"1355";}} @media (min-device-width:1356px){#S:before{content:"1356";}} @media (min-device-width:1357px){#S:before{content:"1357";}} @media (min-device-width:1358px){#S:before{content:"1358";}} @media (min-device-width:1359px){#S:before{content:"1359";}} @media (min-device-width:1360px){#S:before{content:"1360";}} @media (min-device-width:1361px){#S:before{content:"1361";}} @media (min-device-width:1362px){#S:before{content:"1362";}} @media (min-device-width:1363px){#S:before{content:"1363";}} @media (min-device-width:1364px){#S:before{content:"1364";}} @media (min-device-width:1365px){#S:before{content:"1365";}} @media (min-device-width:1366px){#S:before{content:"1366";}} @media (min-device-width:1367px){#S:before{content:"1367";}} @media (min-device-width:1368px){#S:before{content:"1368";}} @media (min-device-width:1369px){#S:before{content:"1369";}} @media (min-device-width:1370px){#S:before{content:"1370";}} @media (min-device-width:1371px){#S:before{content:"1371";}} @media (min-device-width:1372px){#S:before{content:"1372";}} @media (min-device-width:1373px){#S:before{content:"1373";}} @media (min-device-width:1374px){#S:before{content:"1374";}} @media (min-device-width:1375px){#S:before{content:"1375";}} @media (min-device-width:1376px){#S:before{content:"1376";}} @media (min-device-width:1377px){#S:before{content:"1377";}} @media (min-device-width:1378px){#S:before{content:"1378";}} @media (min-device-width:1379px){#S:before{content:"1379";}} @media (min-device-width:1380px){#S:before{content:"1380";}} @media (min-device-width:1381px){#S:before{content:"1381";}} @media (min-device-width:1382px){#S:before{content:"1382";}} @media (min-device-width:1383px){#S:before{content:"1383";}} @media (min-device-width:1384px){#S:before{content:"1384";}} @media (min-device-width:1385px){#S:before{content:"1385";}} @media (min-device-width:1386px){#S:before{content:"1386";}} @media (min-device-width:1387px){#S:before{content:"1387";}} @media (min-device-width:1388px){#S:before{content:"1388";}} @media (min-device-width:1389px){#S:before{content:"1389";}} @media (min-device-width:1390px){#S:before{content:"1390";}} @media (min-device-width:1391px){#S:before{content:"1391";}} @media (min-device-width:1392px){#S:before{content:"1392";}} @media (min-device-width:1393px){#S:before{content:"1393";}} @media (min-device-width:1394px){#S:before{content:"1394";}} @media (min-device-width:1395px){#S:before{content:"1395";}} @media (min-device-width:1396px){#S:before{content:"1396";}} @media (min-device-width:1397px){#S:before{content:"1397";}} @media (min-device-width:1398px){#S:before{content:"1398";}} @media (min-device-width:1399px){#S:before{content:"1399";}} @media (min-device-width:1400px){#S:before{content:"1400";}} @media (min-device-width:1401px){#S:before{content:"1401";}} @media (min-device-width:1402px){#S:before{content:"1402";}} @media (min-device-width:1403px){#S:before{content:"1403";}} @media (min-device-width:1404px){#S:before{content:"1404";}} @media (min-device-width:1405px){#S:before{content:"1405";}} @media (min-device-width:1406px){#S:before{content:"1406";}} @media (min-device-width:1407px){#S:before{content:"1407";}} @media (min-device-width:1408px){#S:before{content:"1408";}} @media (min-device-width:1409px){#S:before{content:"1409";}} @media (min-device-width:1410px){#S:before{content:"1410";}} @media (min-device-width:1411px){#S:before{content:"1411";}} @media (min-device-width:1412px){#S:before{content:"1412";}} @media (min-device-width:1413px){#S:before{content:"1413";}} @media (min-device-width:1414px){#S:before{content:"1414";}} @media (min-device-width:1415px){#S:before{content:"1415";}} @media (min-device-width:1416px){#S:before{content:"1416";}} @media (min-device-width:1417px){#S:before{content:"1417";}} @media (min-device-width:1418px){#S:before{content:"1418";}} @media (min-device-width:1419px){#S:before{content:"1419";}} @media (min-device-width:1420px){#S:before{content:"1420";}} @media (min-device-width:1421px){#S:before{content:"1421";}} @media (min-device-width:1422px){#S:before{content:"1422";}} @media (min-device-width:1423px){#S:before{content:"1423";}} @media (min-device-width:1424px){#S:before{content:"1424";}} @media (min-device-width:1425px){#S:before{content:"1425";}} @media (min-device-width:1426px){#S:before{content:"1426";}} @media (min-device-width:1427px){#S:before{content:"1427";}} @media (min-device-width:1428px){#S:before{content:"1428";}} @media (min-device-width:1429px){#S:before{content:"1429";}} @media (min-device-width:1430px){#S:before{content:"1430";}} @media (min-device-width:1431px){#S:before{content:"1431";}} @media (min-device-width:1432px){#S:before{content:"1432";}} @media (min-device-width:1433px){#S:before{content:"1433";}} @media (min-device-width:1434px){#S:before{content:"1434";}} @media (min-device-width:1435px){#S:before{content:"1435";}} @media (min-device-width:1436px){#S:before{content:"1436";}} @media (min-device-width:1437px){#S:before{content:"1437";}} @media (min-device-width:1438px){#S:before{content:"1438";}} @media (min-device-width:1439px){#S:before{content:"1439";}} @media (min-device-width:1440px){#S:before{content:"1440";}} @media (min-device-width:1441px){#S:before{content:"1441";}} @media (min-device-width:1442px){#S:before{content:"1442";}} @media (min-device-width:1443px){#S:before{content:"1443";}} @media (min-device-width:1444px){#S:before{content:"1444";}} @media (min-device-width:1445px){#S:before{content:"1445";}} @media (min-device-width:1446px){#S:before{content:"1446";}} @media (min-device-width:1447px){#S:before{content:"1447";}} @media (min-device-width:1448px){#S:before{content:"1448";}} @media (min-device-width:1449px){#S:before{content:"1449";}} @media (min-device-width:1450px){#S:before{content:"1450";}} @media (min-device-width:1451px){#S:before{content:"1451";}} @media (min-device-width:1452px){#S:before{content:"1452";}} @media (min-device-width:1453px){#S:before{content:"1453";}} @media (min-device-width:1454px){#S:before{content:"1454";}} @media (min-device-width:1455px){#S:before{content:"1455";}} @media (min-device-width:1456px){#S:before{content:"1456";}} @media (min-device-width:1457px){#S:before{content:"1457";}} @media (min-device-width:1458px){#S:before{content:"1458";}} @media (min-device-width:1459px){#S:before{content:"1459";}} @media (min-device-width:1460px){#S:before{content:"1460";}} @media (min-device-width:1461px){#S:before{content:"1461";}} @media (min-device-width:1462px){#S:before{content:"1462";}} @media (min-device-width:1463px){#S:before{content:"1463";}} @media (min-device-width:1464px){#S:before{content:"1464";}} @media (min-device-width:1465px){#S:before{content:"1465";}} @media (min-device-width:1466px){#S:before{content:"1466";}} @media (min-device-width:1467px){#S:before{content:"1467";}} @media (min-device-width:1468px){#S:before{content:"1468";}} @media (min-device-width:1469px){#S:before{content:"1469";}} @media (min-device-width:1470px){#S:before{content:"1470";}} @media (min-device-width:1471px){#S:before{content:"1471";}} @media (min-device-width:1472px){#S:before{content:"1472";}} @media (min-device-width:1473px){#S:before{content:"1473";}} @media (min-device-width:1474px){#S:before{content:"1474";}} @media (min-device-width:1475px){#S:before{content:"1475";}} @media (min-device-width:1476px){#S:before{content:"1476";}} @media (min-device-width:1477px){#S:before{content:"1477";}} @media (min-device-width:1478px){#S:before{content:"1478";}} @media (min-device-width:1479px){#S:before{content:"1479";}} @media (min-device-width:1480px){#S:before{content:"1480";}} @media (min-device-width:1481px){#S:before{content:"1481";}} @media (min-device-width:1482px){#S:before{content:"1482";}} @media (min-device-width:1483px){#S:before{content:"1483";}} @media (min-device-width:1484px){#S:before{content:"1484";}} @media (min-device-width:1485px){#S:before{content:"1485";}} @media (min-device-width:1486px){#S:before{content:"1486";}} @media (min-device-width:1487px){#S:before{content:"1487";}} @media (min-device-width:1488px){#S:before{content:"1488";}} @media (min-device-width:1489px){#S:before{content:"1489";}} @media (min-device-width:1490px){#S:before{content:"1490";}} @media (min-device-width:1491px){#S:before{content:"1491";}} @media (min-device-width:1492px){#S:before{content:"1492";}} @media (min-device-width:1493px){#S:before{content:"1493";}} @media (min-device-width:1494px){#S:before{content:"1494";}} @media (min-device-width:1495px){#S:before{content:"1495";}} @media (min-device-width:1496px){#S:before{content:"1496";}} @media (min-device-width:1497px){#S:before{content:"1497";}} @media (min-device-width:1498px){#S:before{content:"1498";}} @media (min-device-width:1499px){#S:before{content:"1499";}} @media (min-device-width:1500px){#S:before{content:"1500";}} @media (min-device-width:1501px){#S:before{content:"1501";}} @media (min-device-width:1502px){#S:before{content:"1502";}} @media (min-device-width:1503px){#S:before{content:"1503";}} @media (min-device-width:1504px){#S:before{content:"1504";}} @media (min-device-width:1505px){#S:before{content:"1505";}} @media (min-device-width:1506px){#S:before{content:"1506";}} @media (min-device-width:1507px){#S:before{content:"1507";}} @media (min-device-width:1508px){#S:before{content:"1508";}} @media (min-device-width:1509px){#S:before{content:"1509";}} @media (min-device-width:1510px){#S:before{content:"1510";}} @media (min-device-width:1511px){#S:before{content:"1511";}} @media (min-device-width:1512px){#S:before{content:"1512";}} @media (min-device-width:1513px){#S:before{content:"1513";}} @media (min-device-width:1514px){#S:before{content:"1514";}} @media (min-device-width:1515px){#S:before{content:"1515";}} @media (min-device-width:1516px){#S:before{content:"1516";}} @media (min-device-width:1517px){#S:before{content:"1517";}} @media (min-device-width:1518px){#S:before{content:"1518";}} @media (min-device-width:1519px){#S:before{content:"1519";}} @media (min-device-width:1520px){#S:before{content:"1520";}} @media (min-device-width:1521px){#S:before{content:"1521";}} @media (min-device-width:1522px){#S:before{content:"1522";}} @media (min-device-width:1523px){#S:before{content:"1523";}} @media (min-device-width:1524px){#S:before{content:"1524";}} @media (min-device-width:1525px){#S:before{content:"1525";}} @media (min-device-width:1526px){#S:before{content:"1526";}} @media (min-device-width:1527px){#S:before{content:"1527";}} @media (min-device-width:1528px){#S:before{content:"1528";}} @media (min-device-width:1529px){#S:before{content:"1529";}} @media (min-device-width:1530px){#S:before{content:"1530";}} @media (min-device-width:1531px){#S:before{content:"1531";}} @media (min-device-width:1532px){#S:before{content:"1532";}} @media (min-device-width:1533px){#S:before{content:"1533";}} @media (min-device-width:1534px){#S:before{content:"1534";}} @media (min-device-width:1535px){#S:before{content:"1535";}} @media (min-device-width:1536px){#S:before{content:"1536";}} @media (min-device-width:1537px){#S:before{content:"1537";}} @media (min-device-width:1538px){#S:before{content:"1538";}} @media (min-device-width:1539px){#S:before{content:"1539";}} @media (min-device-width:1540px){#S:before{content:"1540";}} @media (min-device-width:1541px){#S:before{content:"1541";}} @media (min-device-width:1542px){#S:before{content:"1542";}} @media (min-device-width:1543px){#S:before{content:"1543";}} @media (min-device-width:1544px){#S:before{content:"1544";}} @media (min-device-width:1545px){#S:before{content:"1545";}} @media (min-device-width:1546px){#S:before{content:"1546";}} @media (min-device-width:1547px){#S:before{content:"1547";}} @media (min-device-width:1548px){#S:before{content:"1548";}} @media (min-device-width:1549px){#S:before{content:"1549";}} @media (min-device-width:1550px){#S:before{content:"1550";}} @media (min-device-width:1551px){#S:before{content:"1551";}} @media (min-device-width:1552px){#S:before{content:"1552";}} @media (min-device-width:1553px){#S:before{content:"1553";}} @media (min-device-width:1554px){#S:before{content:"1554";}} @media (min-device-width:1555px){#S:before{content:"1555";}} @media (min-device-width:1556px){#S:before{content:"1556";}} @media (min-device-width:1557px){#S:before{content:"1557";}} @media (min-device-width:1558px){#S:before{content:"1558";}} @media (min-device-width:1559px){#S:before{content:"1559";}} @media (min-device-width:1560px){#S:before{content:"1560";}} @media (min-device-width:1561px){#S:before{content:"1561";}} @media (min-device-width:1562px){#S:before{content:"1562";}} @media (min-device-width:1563px){#S:before{content:"1563";}} @media (min-device-width:1564px){#S:before{content:"1564";}} @media (min-device-width:1565px){#S:before{content:"1565";}} @media (min-device-width:1566px){#S:before{content:"1566";}} @media (min-device-width:1567px){#S:before{content:"1567";}} @media (min-device-width:1568px){#S:before{content:"1568";}} @media (min-device-width:1569px){#S:before{content:"1569";}} @media (min-device-width:1570px){#S:before{content:"1570";}} @media (min-device-width:1571px){#S:before{content:"1571";}} @media (min-device-width:1572px){#S:before{content:"1572";}} @media (min-device-width:1573px){#S:before{content:"1573";}} @media (min-device-width:1574px){#S:before{content:"1574";}} @media (min-device-width:1575px){#S:before{content:"1575";}} @media (min-device-width:1576px){#S:before{content:"1576";}} @media (min-device-width:1577px){#S:before{content:"1577";}} @media (min-device-width:1578px){#S:before{content:"1578";}} @media (min-device-width:1579px){#S:before{content:"1579";}} @media (min-device-width:1580px){#S:before{content:"1580";}} @media (min-device-width:1581px){#S:before{content:"1581";}} @media (min-device-width:1582px){#S:before{content:"1582";}} @media (min-device-width:1583px){#S:before{content:"1583";}} @media (min-device-width:1584px){#S:before{content:"1584";}} @media (min-device-width:1585px){#S:before{content:"1585";}} @media (min-device-width:1586px){#S:before{content:"1586";}} @media (min-device-width:1587px){#S:before{content:"1587";}} @media (min-device-width:1588px){#S:before{content:"1588";}} @media (min-device-width:1589px){#S:before{content:"1589";}} @media (min-device-width:1590px){#S:before{content:"1590";}} @media (min-device-width:1591px){#S:before{content:"1591";}} @media (min-device-width:1592px){#S:before{content:"1592";}} @media (min-device-width:1593px){#S:before{content:"1593";}} @media (min-device-width:1594px){#S:before{content:"1594";}} @media (min-device-width:1595px){#S:before{content:"1595";}} @media (min-device-width:1596px){#S:before{content:"1596";}} @media (min-device-width:1597px){#S:before{content:"1597";}} @media (min-device-width:1598px){#S:before{content:"1598";}} @media (min-device-width:1599px){#S:before{content:"1599";}} @media (min-device-width:1600px){#S:before{content:"1600";}} @media (min-device-width:1601px){#S:before{content:"1601";}} @media (min-device-width:1602px){#S:before{content:"1602";}} @media (min-device-width:1603px){#S:before{content:"1603";}} @media (min-device-width:1604px){#S:before{content:"1604";}} @media (min-device-width:1605px){#S:before{content:"1605";}} @media (min-device-width:1606px){#S:before{content:"1606";}} @media (min-device-width:1607px){#S:before{content:"1607";}} @media (min-device-width:1608px){#S:before{content:"1608";}} @media (min-device-width:1609px){#S:before{content:"1609";}} @media (min-device-width:1610px){#S:before{content:"1610";}} @media (min-device-width:1611px){#S:before{content:"1611";}} @media (min-device-width:1612px){#S:before{content:"1612";}} @media (min-device-width:1613px){#S:before{content:"1613";}} @media (min-device-width:1614px){#S:before{content:"1614";}} @media (min-device-width:1615px){#S:before{content:"1615";}} @media (min-device-width:1616px){#S:before{content:"1616";}} @media (min-device-width:1617px){#S:before{content:"1617";}} @media (min-device-width:1618px){#S:before{content:"1618";}} @media (min-device-width:1619px){#S:before{content:"1619";}} @media (min-device-width:1620px){#S:before{content:"1620";}} @media (min-device-width:1621px){#S:before{content:"1621";}} @media (min-device-width:1622px){#S:before{content:"1622";}} @media (min-device-width:1623px){#S:before{content:"1623";}} @media (min-device-width:1624px){#S:before{content:"1624";}} @media (min-device-width:1625px){#S:before{content:"1625";}} @media (min-device-width:1626px){#S:before{content:"1626";}} @media (min-device-width:1627px){#S:before{content:"1627";}} @media (min-device-width:1628px){#S:before{content:"1628";}} @media (min-device-width:1629px){#S:before{content:"1629";}} @media (min-device-width:1630px){#S:before{content:"1630";}} @media (min-device-width:1631px){#S:before{content:"1631";}} @media (min-device-width:1632px){#S:before{content:"1632";}} @media (min-device-width:1633px){#S:before{content:"1633";}} @media (min-device-width:1634px){#S:before{content:"1634";}} @media (min-device-width:1635px){#S:before{content:"1635";}} @media (min-device-width:1636px){#S:before{content:"1636";}} @media (min-device-width:1637px){#S:before{content:"1637";}} @media (min-device-width:1638px){#S:before{content:"1638";}} @media (min-device-width:1639px){#S:before{content:"1639";}} @media (min-device-width:1640px){#S:before{content:"1640";}} @media (min-device-width:1641px){#S:before{content:"1641";}} @media (min-device-width:1642px){#S:before{content:"1642";}} @media (min-device-width:1643px){#S:before{content:"1643";}} @media (min-device-width:1644px){#S:before{content:"1644";}} @media (min-device-width:1645px){#S:before{content:"1645";}} @media (min-device-width:1646px){#S:before{content:"1646";}} @media (min-device-width:1647px){#S:before{content:"1647";}} @media (min-device-width:1648px){#S:before{content:"1648";}} @media (min-device-width:1649px){#S:before{content:"1649";}} @media (min-device-width:1650px){#S:before{content:"1650";}} @media (min-device-width:1651px){#S:before{content:"1651";}} @media (min-device-width:1652px){#S:before{content:"1652";}} @media (min-device-width:1653px){#S:before{content:"1653";}} @media (min-device-width:1654px){#S:before{content:"1654";}} @media (min-device-width:1655px){#S:before{content:"1655";}} @media (min-device-width:1656px){#S:before{content:"1656";}} @media (min-device-width:1657px){#S:before{content:"1657";}} @media (min-device-width:1658px){#S:before{content:"1658";}} @media (min-device-width:1659px){#S:before{content:"1659";}} @media (min-device-width:1660px){#S:before{content:"1660";}} @media (min-device-width:1661px){#S:before{content:"1661";}} @media (min-device-width:1662px){#S:before{content:"1662";}} @media (min-device-width:1663px){#S:before{content:"1663";}} @media (min-device-width:1664px){#S:before{content:"1664";}} @media (min-device-width:1665px){#S:before{content:"1665";}} @media (min-device-width:1666px){#S:before{content:"1666";}} @media (min-device-width:1667px){#S:before{content:"1667";}} @media (min-device-width:1668px){#S:before{content:"1668";}} @media (min-device-width:1669px){#S:before{content:"1669";}} @media (min-device-width:1670px){#S:before{content:"1670";}} @media (min-device-width:1671px){#S:before{content:"1671";}} @media (min-device-width:1672px){#S:before{content:"1672";}} @media (min-device-width:1673px){#S:before{content:"1673";}} @media (min-device-width:1674px){#S:before{content:"1674";}} @media (min-device-width:1675px){#S:before{content:"1675";}} @media (min-device-width:1676px){#S:before{content:"1676";}} @media (min-device-width:1677px){#S:before{content:"1677";}} @media (min-device-width:1678px){#S:before{content:"1678";}} @media (min-device-width:1679px){#S:before{content:"1679";}} @media (min-device-width:1680px){#S:before{content:"1680";}} @media (min-device-width:1681px){#S:before{content:"1681";}} @media (min-device-width:1682px){#S:before{content:"1682";}} @media (min-device-width:1683px){#S:before{content:"1683";}} @media (min-device-width:1684px){#S:before{content:"1684";}} @media (min-device-width:1685px){#S:before{content:"1685";}} @media (min-device-width:1686px){#S:before{content:"1686";}} @media (min-device-width:1687px){#S:before{content:"1687";}} @media (min-device-width:1688px){#S:before{content:"1688";}} @media (min-device-width:1689px){#S:before{content:"1689";}} @media (min-device-width:1690px){#S:before{content:"1690";}} @media (min-device-width:1691px){#S:before{content:"1691";}} @media (min-device-width:1692px){#S:before{content:"1692";}} @media (min-device-width:1693px){#S:before{content:"1693";}} @media (min-device-width:1694px){#S:before{content:"1694";}} @media (min-device-width:1695px){#S:before{content:"1695";}} @media (min-device-width:1696px){#S:before{content:"1696";}} @media (min-device-width:1697px){#S:before{content:"1697";}} @media (min-device-width:1698px){#S:before{content:"1698";}} @media (min-device-width:1699px){#S:before{content:"1699";}} @media (min-device-width:1700px){#S:before{content:"1700";}} @media (min-device-width:1701px){#S:before{content:"1701";}} @media (min-device-width:1702px){#S:before{content:"1702";}} @media (min-device-width:1703px){#S:before{content:"1703";}} @media (min-device-width:1704px){#S:before{content:"1704";}} @media (min-device-width:1705px){#S:before{content:"1705";}} @media (min-device-width:1706px){#S:before{content:"1706";}} @media (min-device-width:1707px){#S:before{content:"1707";}} @media (min-device-width:1708px){#S:before{content:"1708";}} @media (min-device-width:1709px){#S:before{content:"1709";}} @media (min-device-width:1710px){#S:before{content:"1710";}} @media (min-device-width:1711px){#S:before{content:"1711";}} @media (min-device-width:1712px){#S:before{content:"1712";}} @media (min-device-width:1713px){#S:before{content:"1713";}} @media (min-device-width:1714px){#S:before{content:"1714";}} @media (min-device-width:1715px){#S:before{content:"1715";}} @media (min-device-width:1716px){#S:before{content:"1716";}} @media (min-device-width:1717px){#S:before{content:"1717";}} @media (min-device-width:1718px){#S:before{content:"1718";}} @media (min-device-width:1719px){#S:before{content:"1719";}} @media (min-device-width:1720px){#S:before{content:"1720";}} @media (min-device-width:1721px){#S:before{content:"1721";}} @media (min-device-width:1722px){#S:before{content:"1722";}} @media (min-device-width:1723px){#S:before{content:"1723";}} @media (min-device-width:1724px){#S:before{content:"1724";}} @media (min-device-width:1725px){#S:before{content:"1725";}} @media (min-device-width:1726px){#S:before{content:"1726";}} @media (min-device-width:1727px){#S:before{content:"1727";}} @media (min-device-width:1728px){#S:before{content:"1728";}} @media (min-device-width:1729px){#S:before{content:"1729";}} @media (min-device-width:1730px){#S:before{content:"1730";}} @media (min-device-width:1731px){#S:before{content:"1731";}} @media (min-device-width:1732px){#S:before{content:"1732";}} @media (min-device-width:1733px){#S:before{content:"1733";}} @media (min-device-width:1734px){#S:before{content:"1734";}} @media (min-device-width:1735px){#S:before{content:"1735";}} @media (min-device-width:1736px){#S:before{content:"1736";}} @media (min-device-width:1737px){#S:before{content:"1737";}} @media (min-device-width:1738px){#S:before{content:"1738";}} @media (min-device-width:1739px){#S:before{content:"1739";}} @media (min-device-width:1740px){#S:before{content:"1740";}} @media (min-device-width:1741px){#S:before{content:"1741";}} @media (min-device-width:1742px){#S:before{content:"1742";}} @media (min-device-width:1743px){#S:before{content:"1743";}} @media (min-device-width:1744px){#S:before{content:"1744";}} @media (min-device-width:1745px){#S:before{content:"1745";}} @media (min-device-width:1746px){#S:before{content:"1746";}} @media (min-device-width:1747px){#S:before{content:"1747";}} @media (min-device-width:1748px){#S:before{content:"1748";}} @media (min-device-width:1749px){#S:before{content:"1749";}} @media (min-device-width:1750px){#S:before{content:"1750";}} @media (min-device-width:1751px){#S:before{content:"1751";}} @media (min-device-width:1752px){#S:before{content:"1752";}} @media (min-device-width:1753px){#S:before{content:"1753";}} @media (min-device-width:1754px){#S:before{content:"1754";}} @media (min-device-width:1755px){#S:before{content:"1755";}} @media (min-device-width:1756px){#S:before{content:"1756";}} @media (min-device-width:1757px){#S:before{content:"1757";}} @media (min-device-width:1758px){#S:before{content:"1758";}} @media (min-device-width:1759px){#S:before{content:"1759";}} @media (min-device-width:1760px){#S:before{content:"1760";}} @media (min-device-width:1761px){#S:before{content:"1761";}} @media (min-device-width:1762px){#S:before{content:"1762";}} @media (min-device-width:1763px){#S:before{content:"1763";}} @media (min-device-width:1764px){#S:before{content:"1764";}} @media (min-device-width:1765px){#S:before{content:"1765";}} @media (min-device-width:1766px){#S:before{content:"1766";}} @media (min-device-width:1767px){#S:before{content:"1767";}} @media (min-device-width:1768px){#S:before{content:"1768";}} @media (min-device-width:1769px){#S:before{content:"1769";}} @media (min-device-width:1770px){#S:before{content:"1770";}} @media (min-device-width:1771px){#S:before{content:"1771";}} @media (min-device-width:1772px){#S:before{content:"1772";}} @media (min-device-width:1773px){#S:before{content:"1773";}} @media (min-device-width:1774px){#S:before{content:"1774";}} @media (min-device-width:1775px){#S:before{content:"1775";}} @media (min-device-width:1776px){#S:before{content:"1776";}} @media (min-device-width:1777px){#S:before{content:"1777";}} @media (min-device-width:1778px){#S:before{content:"1778";}} @media (min-device-width:1779px){#S:before{content:"1779";}} @media (min-device-width:1780px){#S:before{content:"1780";}} @media (min-device-width:1781px){#S:before{content:"1781";}} @media (min-device-width:1782px){#S:before{content:"1782";}} @media (min-device-width:1783px){#S:before{content:"1783";}} @media (min-device-width:1784px){#S:before{content:"1784";}} @media (min-device-width:1785px){#S:before{content:"1785";}} @media (min-device-width:1786px){#S:before{content:"1786";}} @media (min-device-width:1787px){#S:before{content:"1787";}} @media (min-device-width:1788px){#S:before{content:"1788";}} @media (min-device-width:1789px){#S:before{content:"1789";}} @media (min-device-width:1790px){#S:before{content:"1790";}} @media (min-device-width:1791px){#S:before{content:"1791";}} @media (min-device-width:1792px){#S:before{content:"1792";}} @media (min-device-width:1793px){#S:before{content:"1793";}} @media (min-device-width:1794px){#S:before{content:"1794";}} @media (min-device-width:1795px){#S:before{content:"1795";}} @media (min-device-width:1796px){#S:before{content:"1796";}} @media (min-device-width:1797px){#S:before{content:"1797";}} @media (min-device-width:1798px){#S:before{content:"1798";}} @media (min-device-width:1799px){#S:before{content:"1799";}} @media (min-device-width:1800px){#S:before{content:"1800";}} @media (min-device-width:1801px){#S:before{content:"1801";}} @media (min-device-width:1802px){#S:before{content:"1802";}} @media (min-device-width:1803px){#S:before{content:"1803";}} @media (min-device-width:1804px){#S:before{content:"1804";}} @media (min-device-width:1805px){#S:before{content:"1805";}} @media (min-device-width:1806px){#S:before{content:"1806";}} @media (min-device-width:1807px){#S:before{content:"1807";}} @media (min-device-width:1808px){#S:before{content:"1808";}} @media (min-device-width:1809px){#S:before{content:"1809";}} @media (min-device-width:1810px){#S:before{content:"1810";}} @media (min-device-width:1811px){#S:before{content:"1811";}} @media (min-device-width:1812px){#S:before{content:"1812";}} @media (min-device-width:1813px){#S:before{content:"1813";}} @media (min-device-width:1814px){#S:before{content:"1814";}} @media (min-device-width:1815px){#S:before{content:"1815";}} @media (min-device-width:1816px){#S:before{content:"1816";}} @media (min-device-width:1817px){#S:before{content:"1817";}} @media (min-device-width:1818px){#S:before{content:"1818";}} @media (min-device-width:1819px){#S:before{content:"1819";}} @media (min-device-width:1820px){#S:before{content:"1820";}} @media (min-device-width:1821px){#S:before{content:"1821";}} @media (min-device-width:1822px){#S:before{content:"1822";}} @media (min-device-width:1823px){#S:before{content:"1823";}} @media (min-device-width:1824px){#S:before{content:"1824";}} @media (min-device-width:1825px){#S:before{content:"1825";}} @media (min-device-width:1826px){#S:before{content:"1826";}} @media (min-device-width:1827px){#S:before{content:"1827";}} @media (min-device-width:1828px){#S:before{content:"1828";}} @media (min-device-width:1829px){#S:before{content:"1829";}} @media (min-device-width:1830px){#S:before{content:"1830";}} @media (min-device-width:1831px){#S:before{content:"1831";}} @media (min-device-width:1832px){#S:before{content:"1832";}} @media (min-device-width:1833px){#S:before{content:"1833";}} @media (min-device-width:1834px){#S:before{content:"1834";}} @media (min-device-width:1835px){#S:before{content:"1835";}} @media (min-device-width:1836px){#S:before{content:"1836";}} @media (min-device-width:1837px){#S:before{content:"1837";}} @media (min-device-width:1838px){#S:before{content:"1838";}} @media (min-device-width:1839px){#S:before{content:"1839";}} @media (min-device-width:1840px){#S:before{content:"1840";}} @media (min-device-width:1841px){#S:before{content:"1841";}} @media (min-device-width:1842px){#S:before{content:"1842";}} @media (min-device-width:1843px){#S:before{content:"1843";}} @media (min-device-width:1844px){#S:before{content:"1844";}} @media (min-device-width:1845px){#S:before{content:"1845";}} @media (min-device-width:1846px){#S:before{content:"1846";}} @media (min-device-width:1847px){#S:before{content:"1847";}} @media (min-device-width:1848px){#S:before{content:"1848";}} @media (min-device-width:1849px){#S:before{content:"1849";}} @media (min-device-width:1850px){#S:before{content:"1850";}} @media (min-device-width:1851px){#S:before{content:"1851";}} @media (min-device-width:1852px){#S:before{content:"1852";}} @media (min-device-width:1853px){#S:before{content:"1853";}} @media (min-device-width:1854px){#S:before{content:"1854";}} @media (min-device-width:1855px){#S:before{content:"1855";}} @media (min-device-width:1856px){#S:before{content:"1856";}} @media (min-device-width:1857px){#S:before{content:"1857";}} @media (min-device-width:1858px){#S:before{content:"1858";}} @media (min-device-width:1859px){#S:before{content:"1859";}} @media (min-device-width:1860px){#S:before{content:"1860";}} @media (min-device-width:1861px){#S:before{content:"1861";}} @media (min-device-width:1862px){#S:before{content:"1862";}} @media (min-device-width:1863px){#S:before{content:"1863";}} @media (min-device-width:1864px){#S:before{content:"1864";}} @media (min-device-width:1865px){#S:before{content:"1865";}} @media (min-device-width:1866px){#S:before{content:"1866";}} @media (min-device-width:1867px){#S:before{content:"1867";}} @media (min-device-width:1868px){#S:before{content:"1868";}} @media (min-device-width:1869px){#S:before{content:"1869";}} @media (min-device-width:1870px){#S:before{content:"1870";}} @media (min-device-width:1871px){#S:before{content:"1871";}} @media (min-device-width:1872px){#S:before{content:"1872";}} @media (min-device-width:1873px){#S:before{content:"1873";}} @media (min-device-width:1874px){#S:before{content:"1874";}} @media (min-device-width:1875px){#S:before{content:"1875";}} @media (min-device-width:1876px){#S:before{content:"1876";}} @media (min-device-width:1877px){#S:before{content:"1877";}} @media (min-device-width:1878px){#S:before{content:"1878";}} @media (min-device-width:1879px){#S:before{content:"1879";}} @media (min-device-width:1880px){#S:before{content:"1880";}} @media (min-device-width:1881px){#S:before{content:"1881";}} @media (min-device-width:1882px){#S:before{content:"1882";}} @media (min-device-width:1883px){#S:before{content:"1883";}} @media (min-device-width:1884px){#S:before{content:"1884";}} @media (min-device-width:1885px){#S:before{content:"1885";}} @media (min-device-width:1886px){#S:before{content:"1886";}} @media (min-device-width:1887px){#S:before{content:"1887";}} @media (min-device-width:1888px){#S:before{content:"1888";}} @media (min-device-width:1889px){#S:before{content:"1889";}} @media (min-device-width:1890px){#S:before{content:"1890";}} @media (min-device-width:1891px){#S:before{content:"1891";}} @media (min-device-width:1892px){#S:before{content:"1892";}} @media (min-device-width:1893px){#S:before{content:"1893";}} @media (min-device-width:1894px){#S:before{content:"1894";}} @media (min-device-width:1895px){#S:before{content:"1895";}} @media (min-device-width:1896px){#S:before{content:"1896";}} @media (min-device-width:1897px){#S:before{content:"1897";}} @media (min-device-width:1898px){#S:before{content:"1898";}} @media (min-device-width:1899px){#S:before{content:"1899";}} @media (min-device-width:1900px){#S:before{content:"1900";}} @media (min-device-width:1901px){#S:before{content:"1901";}} @media (min-device-width:1902px){#S:before{content:"1902";}} @media (min-device-width:1903px){#S:before{content:"1903";}} @media (min-device-width:1904px){#S:before{content:"1904";}} @media (min-device-width:1905px){#S:before{content:"1905";}} @media (min-device-width:1906px){#S:before{content:"1906";}} @media (min-device-width:1907px){#S:before{content:"1907";}} @media (min-device-width:1908px){#S:before{content:"1908";}} @media (min-device-width:1909px){#S:before{content:"1909";}} @media (min-device-width:1910px){#S:before{content:"1910";}} @media (min-device-width:1911px){#S:before{content:"1911";}} @media (min-device-width:1912px){#S:before{content:"1912";}} @media (min-device-width:1913px){#S:before{content:"1913";}} @media (min-device-width:1914px){#S:before{content:"1914";}} @media (min-device-width:1915px){#S:before{content:"1915";}} @media (min-device-width:1916px){#S:before{content:"1916";}} @media (min-device-width:1917px){#S:before{content:"1917";}} @media (min-device-width:1918px){#S:before{content:"1918";}} @media (min-device-width:1919px){#S:before{content:"1919";}} @media (min-device-width:1920px){#S:before{content:"1920";}} @media (min-device-width:1921px){#S:before{content:"1921";}} @media (min-device-width:1922px){#S:before{content:"1922";}} @media (min-device-width:1923px){#S:before{content:"1923";}} @media (min-device-width:1924px){#S:before{content:"1924";}} @media (min-device-width:1925px){#S:before{content:"1925";}} @media (min-device-width:1926px){#S:before{content:"1926";}} @media (min-device-width:1927px){#S:before{content:"1927";}} @media (min-device-width:1928px){#S:before{content:"1928";}} @media (min-device-width:1929px){#S:before{content:"1929";}} @media (min-device-width:1930px){#S:before{content:"1930";}} @media (min-device-width:1931px){#S:before{content:"1931";}} @media (min-device-width:1932px){#S:before{content:"1932";}} @media (min-device-width:1933px){#S:before{content:"1933";}} @media (min-device-width:1934px){#S:before{content:"1934";}} @media (min-device-width:1935px){#S:before{content:"1935";}} @media (min-device-width:1936px){#S:before{content:"1936";}} @media (min-device-width:1937px){#S:before{content:"1937";}} @media (min-device-width:1938px){#S:before{content:"1938";}} @media (min-device-width:1939px){#S:before{content:"1939";}} @media (min-device-width:1940px){#S:before{content:"1940";}} @media (min-device-width:1941px){#S:before{content:"1941";}} @media (min-device-width:1942px){#S:before{content:"1942";}} @media (min-device-width:1943px){#S:before{content:"1943";}} @media (min-device-width:1944px){#S:before{content:"1944";}} @media (min-device-width:1945px){#S:before{content:"1945";}} @media (min-device-width:1946px){#S:before{content:"1946";}} @media (min-device-width:1947px){#S:before{content:"1947";}} @media (min-device-width:1948px){#S:before{content:"1948";}} @media (min-device-width:1949px){#S:before{content:"1949";}} @media (min-device-width:1950px){#S:before{content:"1950";}} @media (min-device-width:1951px){#S:before{content:"1951";}} @media (min-device-width:1952px){#S:before{content:"1952";}} @media (min-device-width:1953px){#S:before{content:"1953";}} @media (min-device-width:1954px){#S:before{content:"1954";}} @media (min-device-width:1955px){#S:before{content:"1955";}} @media (min-device-width:1956px){#S:before{content:"1956";}} @media (min-device-width:1957px){#S:before{content:"1957";}} @media (min-device-width:1958px){#S:before{content:"1958";}} @media (min-device-width:1959px){#S:before{content:"1959";}} @media (min-device-width:1960px){#S:before{content:"1960";}} @media (min-device-width:1961px){#S:before{content:"1961";}} @media (min-device-width:1962px){#S:before{content:"1962";}} @media (min-device-width:1963px){#S:before{content:"1963";}} @media (min-device-width:1964px){#S:before{content:"1964";}} @media (min-device-width:1965px){#S:before{content:"1965";}} @media (min-device-width:1966px){#S:before{content:"1966";}} @media (min-device-width:1967px){#S:before{content:"1967";}} @media (min-device-width:1968px){#S:before{content:"1968";}} @media (min-device-width:1969px){#S:before{content:"1969";}} @media (min-device-width:1970px){#S:before{content:"1970";}} @media (min-device-width:1971px){#S:before{content:"1971";}} @media (min-device-width:1972px){#S:before{content:"1972";}} @media (min-device-width:1973px){#S:before{content:"1973";}} @media (min-device-width:1974px){#S:before{content:"1974";}} @media (min-device-width:1975px){#S:before{content:"1975";}} @media (min-device-width:1976px){#S:before{content:"1976";}} @media (min-device-width:1977px){#S:before{content:"1977";}} @media (min-device-width:1978px){#S:before{content:"1978";}} @media (min-device-width:1979px){#S:before{content:"1979";}} @media (min-device-width:1980px){#S:before{content:"1980";}} @media (min-device-width:1981px){#S:before{content:"1981";}} @media (min-device-width:1982px){#S:before{content:"1982";}} @media (min-device-width:1983px){#S:before{content:"1983";}} @media (min-device-width:1984px){#S:before{content:"1984";}} @media (min-device-width:1985px){#S:before{content:"1985";}} @media (min-device-width:1986px){#S:before{content:"1986";}} @media (min-device-width:1987px){#S:before{content:"1987";}} @media (min-device-width:1988px){#S:before{content:"1988";}} @media (min-device-width:1989px){#S:before{content:"1989";}} @media (min-device-width:1990px){#S:before{content:"1990";}} @media (min-device-width:1991px){#S:before{content:"1991";}} @media (min-device-width:1992px){#S:before{content:"1992";}} @media (min-device-width:1993px){#S:before{content:"1993";}} @media (min-device-width:1994px){#S:before{content:"1994";}} @media (min-device-width:1995px){#S:before{content:"1995";}} @media (min-device-width:1996px){#S:before{content:"1996";}} @media (min-device-width:1997px){#S:before{content:"1997";}} @media (min-device-width:1998px){#S:before{content:"1998";}} @media (min-device-width:1999px){#S:before{content:"1999";}} @media (min-device-width:2000px){#S:before{content:"2000";}} @media (min-device-width:2001px){#S:before{content:"2001";}} @media (min-device-width:2002px){#S:before{content:"2002";}} @media (min-device-width:2003px){#S:before{content:"2003";}} @media (min-device-width:2004px){#S:before{content:"2004";}} @media (min-device-width:2005px){#S:before{content:"2005";}} @media (min-device-width:2006px){#S:before{content:"2006";}} @media (min-device-width:2007px){#S:before{content:"2007";}} @media (min-device-width:2008px){#S:before{content:"2008";}} @media (min-device-width:2009px){#S:before{content:"2009";}} @media (min-device-width:2010px){#S:before{content:"2010";}} @media (min-device-width:2011px){#S:before{content:"2011";}} @media (min-device-width:2012px){#S:before{content:"2012";}} @media (min-device-width:2013px){#S:before{content:"2013";}} @media (min-device-width:2014px){#S:before{content:"2014";}} @media (min-device-width:2015px){#S:before{content:"2015";}} @media (min-device-width:2016px){#S:before{content:"2016";}} @media (min-device-width:2017px){#S:before{content:"2017";}} @media (min-device-width:2018px){#S:before{content:"2018";}} @media (min-device-width:2019px){#S:before{content:"2019";}} @media (min-device-width:2020px){#S:before{content:"2020";}} @media (min-device-width:2021px){#S:before{content:"2021";}} @media (min-device-width:2022px){#S:before{content:"2022";}} @media (min-device-width:2023px){#S:before{content:"2023";}} @media (min-device-width:2024px){#S:before{content:"2024";}} @media (min-device-width:2025px){#S:before{content:"2025";}} @media (min-device-width:2026px){#S:before{content:"2026";}} @media (min-device-width:2027px){#S:before{content:"2027";}} @media (min-device-width:2028px){#S:before{content:"2028";}} @media (min-device-width:2029px){#S:before{content:"2029";}} @media (min-device-width:2030px){#S:before{content:"2030";}} @media (min-device-width:2031px){#S:before{content:"2031";}} @media (min-device-width:2032px){#S:before{content:"2032";}} @media (min-device-width:2033px){#S:before{content:"2033";}} @media (min-device-width:2034px){#S:before{content:"2034";}} @media (min-device-width:2035px){#S:before{content:"2035";}} @media (min-device-width:2036px){#S:before{content:"2036";}} @media (min-device-width:2037px){#S:before{content:"2037";}} @media (min-device-width:2038px){#S:before{content:"2038";}} @media (min-device-width:2039px){#S:before{content:"2039";}} @media (min-device-width:2040px){#S:before{content:"2040";}} @media (min-device-width:2041px){#S:before{content:"2041";}} @media (min-device-width:2042px){#S:before{content:"2042";}} @media (min-device-width:2043px){#S:before{content:"2043";}} @media (min-device-width:2044px){#S:before{content:"2044";}} @media (min-device-width:2045px){#S:before{content:"2045";}} @media (min-device-width:2046px){#S:before{content:"2046";}} @media (min-device-width:2047px){#S:before{content:"2047";}} @media (min-device-width:2048px){#S:before{content:"2048";}} @media (min-device-width:2049px){#S:before{content:"2049";}} @media (min-device-width:2050px){#S:before{content:"2050";}} @media (min-device-width:2051px){#S:before{content:"2051";}} @media (min-device-width:2052px){#S:before{content:"2052";}} @media (min-device-width:2053px){#S:before{content:"2053";}} @media (min-device-width:2054px){#S:before{content:"2054";}} @media (min-device-width:2055px){#S:before{content:"2055";}} @media (min-device-width:2056px){#S:before{content:"2056";}} @media (min-device-width:2057px){#S:before{content:"2057";}} @media (min-device-width:2058px){#S:before{content:"2058";}} @media (min-device-width:2059px){#S:before{content:"2059";}} @media (min-device-width:2060px){#S:before{content:"2060";}} @media (min-device-width:2061px){#S:before{content:"2061";}} @media (min-device-width:2062px){#S:before{content:"2062";}} @media (min-device-width:2063px){#S:before{content:"2063";}} @media (min-device-width:2064px){#S:before{content:"2064";}} @media (min-device-width:2065px){#S:before{content:"2065";}} @media (min-device-width:2066px){#S:before{content:"2066";}} @media (min-device-width:2067px){#S:before{content:"2067";}} @media (min-device-width:2068px){#S:before{content:"2068";}} @media (min-device-width:2069px){#S:before{content:"2069";}} @media (min-device-width:2070px){#S:before{content:"2070";}} @media (min-device-width:2071px){#S:before{content:"2071";}} @media (min-device-width:2072px){#S:before{content:"2072";}} @media (min-device-width:2073px){#S:before{content:"2073";}} @media (min-device-width:2074px){#S:before{content:"2074";}} @media (min-device-width:2075px){#S:before{content:"2075";}} @media (min-device-width:2076px){#S:before{content:"2076";}} @media (min-device-width:2077px){#S:before{content:"2077";}} @media (min-device-width:2078px){#S:before{content:"2078";}} @media (min-device-width:2079px){#S:before{content:"2079";}} @media (min-device-width:2080px){#S:before{content:"2080";}} @media (min-device-width:2081px){#S:before{content:"2081";}} @media (min-device-width:2082px){#S:before{content:"2082";}} @media (min-device-width:2083px){#S:before{content:"2083";}} @media (min-device-width:2084px){#S:before{content:"2084";}} @media (min-device-width:2085px){#S:before{content:"2085";}} @media (min-device-width:2086px){#S:before{content:"2086";}} @media (min-device-width:2087px){#S:before{content:"2087";}} @media (min-device-width:2088px){#S:before{content:"2088";}} @media (min-device-width:2089px){#S:before{content:"2089";}} @media (min-device-width:2090px){#S:before{content:"2090";}} @media (min-device-width:2091px){#S:before{content:"2091";}} @media (min-device-width:2092px){#S:before{content:"2092";}} @media (min-device-width:2093px){#S:before{content:"2093";}} @media (min-device-width:2094px){#S:before{content:"2094";}} @media (min-device-width:2095px){#S:before{content:"2095";}} @media (min-device-width:2096px){#S:before{content:"2096";}} @media (min-device-width:2097px){#S:before{content:"2097";}} @media (min-device-width:2098px){#S:before{content:"2098";}} @media (min-device-width:2099px){#S:before{content:"2099";}} @media (min-device-width:2100px){#S:before{content:"2100";}} @media (min-device-width:2101px){#S:before{content:"2101";}} @media (min-device-width:2102px){#S:before{content:"2102";}} @media (min-device-width:2103px){#S:before{content:"2103";}} @media (min-device-width:2104px){#S:before{content:"2104";}} @media (min-device-width:2105px){#S:before{content:"2105";}} @media (min-device-width:2106px){#S:before{content:"2106";}} @media (min-device-width:2107px){#S:before{content:"2107";}} @media (min-device-width:2108px){#S:before{content:"2108";}} @media (min-device-width:2109px){#S:before{content:"2109";}} @media (min-device-width:2110px){#S:before{content:"2110";}} @media (min-device-width:2111px){#S:before{content:"2111";}} @media (min-device-width:2112px){#S:before{content:"2112";}} @media (min-device-width:2113px){#S:before{content:"2113";}} @media (min-device-width:2114px){#S:before{content:"2114";}} @media (min-device-width:2115px){#S:before{content:"2115";}} @media (min-device-width:2116px){#S:before{content:"2116";}} @media (min-device-width:2117px){#S:before{content:"2117";}} @media (min-device-width:2118px){#S:before{content:"2118";}} @media (min-device-width:2119px){#S:before{content:"2119";}} @media (min-device-width:2120px){#S:before{content:"2120";}} @media (min-device-width:2121px){#S:before{content:"2121";}} @media (min-device-width:2122px){#S:before{content:"2122";}} @media (min-device-width:2123px){#S:before{content:"2123";}} @media (min-device-width:2124px){#S:before{content:"2124";}} @media (min-device-width:2125px){#S:before{content:"2125";}} @media (min-device-width:2126px){#S:before{content:"2126";}} @media (min-device-width:2127px){#S:before{content:"2127";}} @media (min-device-width:2128px){#S:before{content:"2128";}} @media (min-device-width:2129px){#S:before{content:"2129";}} @media (min-device-width:2130px){#S:before{content:"2130";}} @media (min-device-width:2131px){#S:before{content:"2131";}} @media (min-device-width:2132px){#S:before{content:"2132";}} @media (min-device-width:2133px){#S:before{content:"2133";}} @media (min-device-width:2134px){#S:before{content:"2134";}} @media (min-device-width:2135px){#S:before{content:"2135";}} @media (min-device-width:2136px){#S:before{content:"2136";}} @media (min-device-width:2137px){#S:before{content:"2137";}} @media (min-device-width:2138px){#S:before{content:"2138";}} @media (min-device-width:2139px){#S:before{content:"2139";}} @media (min-device-width:2140px){#S:before{content:"2140";}} @media (min-device-width:2141px){#S:before{content:"2141";}} @media (min-device-width:2142px){#S:before{content:"2142";}} @media (min-device-width:2143px){#S:before{content:"2143";}} @media (min-device-width:2144px){#S:before{content:"2144";}} @media (min-device-width:2145px){#S:before{content:"2145";}} @media (min-device-width:2146px){#S:before{content:"2146";}} @media (min-device-width:2147px){#S:before{content:"2147";}} @media (min-device-width:2148px){#S:before{content:"2148";}} @media (min-device-width:2149px){#S:before{content:"2149";}} @media (min-device-width:2150px){#S:before{content:"2150";}} @media (min-device-width:2151px){#S:before{content:"2151";}} @media (min-device-width:2152px){#S:before{content:"2152";}} @media (min-device-width:2153px){#S:before{content:"2153";}} @media (min-device-width:2154px){#S:before{content:"2154";}} @media (min-device-width:2155px){#S:before{content:"2155";}} @media (min-device-width:2156px){#S:before{content:"2156";}} @media (min-device-width:2157px){#S:before{content:"2157";}} @media (min-device-width:2158px){#S:before{content:"2158";}} @media (min-device-width:2159px){#S:before{content:"2159";}} @media (min-device-width:2160px){#S:before{content:"2160";}} @media (min-device-width:2161px){#S:before{content:"2161";}} @media (min-device-width:2162px){#S:before{content:"2162";}} @media (min-device-width:2163px){#S:before{content:"2163";}} @media (min-device-width:2164px){#S:before{content:"2164";}} @media (min-device-width:2165px){#S:before{content:"2165";}} @media (min-device-width:2166px){#S:before{content:"2166";}} @media (min-device-width:2167px){#S:before{content:"2167";}} @media (min-device-width:2168px){#S:before{content:"2168";}} @media (min-device-width:2169px){#S:before{content:"2169";}} @media (min-device-width:2170px){#S:before{content:"2170";}} @media (min-device-width:2171px){#S:before{content:"2171";}} @media (min-device-width:2172px){#S:before{content:"2172";}} @media (min-device-width:2173px){#S:before{content:"2173";}} @media (min-device-width:2174px){#S:before{content:"2174";}} @media (min-device-width:2175px){#S:before{content:"2175";}} @media (min-device-width:2176px){#S:before{content:"2176";}} @media (min-device-width:2177px){#S:before{content:"2177";}} @media (min-device-width:2178px){#S:before{content:"2178";}} @media (min-device-width:2179px){#S:before{content:"2179";}} @media (min-device-width:2180px){#S:before{content:"2180";}} @media (min-device-width:2181px){#S:before{content:"2181";}} @media (min-device-width:2182px){#S:before{content:"2182";}} @media (min-device-width:2183px){#S:before{content:"2183";}} @media (min-device-width:2184px){#S:before{content:"2184";}} @media (min-device-width:2185px){#S:before{content:"2185";}} @media (min-device-width:2186px){#S:before{content:"2186";}} @media (min-device-width:2187px){#S:before{content:"2187";}} @media (min-device-width:2188px){#S:before{content:"2188";}} @media (min-device-width:2189px){#S:before{content:"2189";}} @media (min-device-width:2190px){#S:before{content:"2190";}} @media (min-device-width:2191px){#S:before{content:"2191";}} @media (min-device-width:2192px){#S:before{content:"2192";}} @media (min-device-width:2193px){#S:before{content:"2193";}} @media (min-device-width:2194px){#S:before{content:"2194";}} @media (min-device-width:2195px){#S:before{content:"2195";}} @media (min-device-width:2196px){#S:before{content:"2196";}} @media (min-device-width:2197px){#S:before{content:"2197";}} @media (min-device-width:2198px){#S:before{content:"2198";}} @media (min-device-width:2199px){#S:before{content:"2199";}} @media (min-device-width:2200px){#S:before{content:"2200";}} @media (min-device-width:2201px){#S:before{content:"2201";}} @media (min-device-width:2202px){#S:before{content:"2202";}} @media (min-device-width:2203px){#S:before{content:"2203";}} @media (min-device-width:2204px){#S:before{content:"2204";}} @media (min-device-width:2205px){#S:before{content:"2205";}} @media (min-device-width:2206px){#S:before{content:"2206";}} @media (min-device-width:2207px){#S:before{content:"2207";}} @media (min-device-width:2208px){#S:before{content:"2208";}} @media (min-device-width:2209px){#S:before{content:"2209";}} @media (min-device-width:2210px){#S:before{content:"2210";}} @media (min-device-width:2211px){#S:before{content:"2211";}} @media (min-device-width:2212px){#S:before{content:"2212";}} @media (min-device-width:2213px){#S:before{content:"2213";}} @media (min-device-width:2214px){#S:before{content:"2214";}} @media (min-device-width:2215px){#S:before{content:"2215";}} @media (min-device-width:2216px){#S:before{content:"2216";}} @media (min-device-width:2217px){#S:before{content:"2217";}} @media (min-device-width:2218px){#S:before{content:"2218";}} @media (min-device-width:2219px){#S:before{content:"2219";}} @media (min-device-width:2220px){#S:before{content:"2220";}} @media (min-device-width:2221px){#S:before{content:"2221";}} @media (min-device-width:2222px){#S:before{content:"2222";}} @media (min-device-width:2223px){#S:before{content:"2223";}} @media (min-device-width:2224px){#S:before{content:"2224";}} @media (min-device-width:2225px){#S:before{content:"2225";}} @media (min-device-width:2226px){#S:before{content:"2226";}} @media (min-device-width:2227px){#S:before{content:"2227";}} @media (min-device-width:2228px){#S:before{content:"2228";}} @media (min-device-width:2229px){#S:before{content:"2229";}} @media (min-device-width:2230px){#S:before{content:"2230";}} @media (min-device-width:2231px){#S:before{content:"2231";}} @media (min-device-width:2232px){#S:before{content:"2232";}} @media (min-device-width:2233px){#S:before{content:"2233";}} @media (min-device-width:2234px){#S:before{content:"2234";}} @media (min-device-width:2235px){#S:before{content:"2235";}} @media (min-device-width:2236px){#S:before{content:"2236";}} @media (min-device-width:2237px){#S:before{content:"2237";}} @media (min-device-width:2238px){#S:before{content:"2238";}} @media (min-device-width:2239px){#S:before{content:"2239";}} @media (min-device-width:2240px){#S:before{content:"2240";}} @media (min-device-width:2241px){#S:before{content:"2241";}} @media (min-device-width:2242px){#S:before{content:"2242";}} @media (min-device-width:2243px){#S:before{content:"2243";}} @media (min-device-width:2244px){#S:before{content:"2244";}} @media (min-device-width:2245px){#S:before{content:"2245";}} @media (min-device-width:2246px){#S:before{content:"2246";}} @media (min-device-width:2247px){#S:before{content:"2247";}} @media (min-device-width:2248px){#S:before{content:"2248";}} @media (min-device-width:2249px){#S:before{content:"2249";}} @media (min-device-width:2250px){#S:before{content:"2250";}} @media (min-device-width:2251px){#S:before{content:"2251";}} @media (min-device-width:2252px){#S:before{content:"2252";}} @media (min-device-width:2253px){#S:before{content:"2253";}} @media (min-device-width:2254px){#S:before{content:"2254";}} @media (min-device-width:2255px){#S:before{content:"2255";}} @media (min-device-width:2256px){#S:before{content:"2256";}} @media (min-device-width:2257px){#S:before{content:"2257";}} @media (min-device-width:2258px){#S:before{content:"2258";}} @media (min-device-width:2259px){#S:before{content:"2259";}} @media (min-device-width:2260px){#S:before{content:"2260";}} @media (min-device-width:2261px){#S:before{content:"2261";}} @media (min-device-width:2262px){#S:before{content:"2262";}} @media (min-device-width:2263px){#S:before{content:"2263";}} @media (min-device-width:2264px){#S:before{content:"2264";}} @media (min-device-width:2265px){#S:before{content:"2265";}} @media (min-device-width:2266px){#S:before{content:"2266";}} @media (min-device-width:2267px){#S:before{content:"2267";}} @media (min-device-width:2268px){#S:before{content:"2268";}} @media (min-device-width:2269px){#S:before{content:"2269";}} @media (min-device-width:2270px){#S:before{content:"2270";}} @media (min-device-width:2271px){#S:before{content:"2271";}} @media (min-device-width:2272px){#S:before{content:"2272";}} @media (min-device-width:2273px){#S:before{content:"2273";}} @media (min-device-width:2274px){#S:before{content:"2274";}} @media (min-device-width:2275px){#S:before{content:"2275";}} @media (min-device-width:2276px){#S:before{content:"2276";}} @media (min-device-width:2277px){#S:before{content:"2277";}} @media (min-device-width:2278px){#S:before{content:"2278";}} @media (min-device-width:2279px){#S:before{content:"2279";}} @media (min-device-width:2280px){#S:before{content:"2280";}} @media (min-device-width:2281px){#S:before{content:"2281";}} @media (min-device-width:2282px){#S:before{content:"2282";}} @media (min-device-width:2283px){#S:before{content:"2283";}} @media (min-device-width:2284px){#S:before{content:"2284";}} @media (min-device-width:2285px){#S:before{content:"2285";}} @media (min-device-width:2286px){#S:before{content:"2286";}} @media (min-device-width:2287px){#S:before{content:"2287";}} @media (min-device-width:2288px){#S:before{content:"2288";}} @media (min-device-width:2289px){#S:before{content:"2289";}} @media (min-device-width:2290px){#S:before{content:"2290";}} @media (min-device-width:2291px){#S:before{content:"2291";}} @media (min-device-width:2292px){#S:before{content:"2292";}} @media (min-device-width:2293px){#S:before{content:"2293";}} @media (min-device-width:2294px){#S:before{content:"2294";}} @media (min-device-width:2295px){#S:before{content:"2295";}} @media (min-device-width:2296px){#S:before{content:"2296";}} @media (min-device-width:2297px){#S:before{content:"2297";}} @media (min-device-width:2298px){#S:before{content:"2298";}} @media (min-device-width:2299px){#S:before{content:"2299";}} @media (min-device-width:2300px){#S:before{content:"2300";}} @media (min-device-width:2301px){#S:before{content:"2301";}} @media (min-device-width:2302px){#S:before{content:"2302";}} @media (min-device-width:2303px){#S:before{content:"2303";}} @media (min-device-width:2304px){#S:before{content:"2304";}} @media (min-device-width:2305px){#S:before{content:"2305";}} @media (min-device-width:2306px){#S:before{content:"2306";}} @media (min-device-width:2307px){#S:before{content:"2307";}} @media (min-device-width:2308px){#S:before{content:"2308";}} @media (min-device-width:2309px){#S:before{content:"2309";}} @media (min-device-width:2310px){#S:before{content:"2310";}} @media (min-device-width:2311px){#S:before{content:"2311";}} @media (min-device-width:2312px){#S:before{content:"2312";}} @media (min-device-width:2313px){#S:before{content:"2313";}} @media (min-device-width:2314px){#S:before{content:"2314";}} @media (min-device-width:2315px){#S:before{content:"2315";}} @media (min-device-width:2316px){#S:before{content:"2316";}} @media (min-device-width:2317px){#S:before{content:"2317";}} @media (min-device-width:2318px){#S:before{content:"2318";}} @media (min-device-width:2319px){#S:before{content:"2319";}} @media (min-device-width:2320px){#S:before{content:"2320";}} @media (min-device-width:2321px){#S:before{content:"2321";}} @media (min-device-width:2322px){#S:before{content:"2322";}} @media (min-device-width:2323px){#S:before{content:"2323";}} @media (min-device-width:2324px){#S:before{content:"2324";}} @media (min-device-width:2325px){#S:before{content:"2325";}} @media (min-device-width:2326px){#S:before{content:"2326";}} @media (min-device-width:2327px){#S:before{content:"2327";}} @media (min-device-width:2328px){#S:before{content:"2328";}} @media (min-device-width:2329px){#S:before{content:"2329";}} @media (min-device-width:2330px){#S:before{content:"2330";}} @media (min-device-width:2331px){#S:before{content:"2331";}} @media (min-device-width:2332px){#S:before{content:"2332";}} @media (min-device-width:2333px){#S:before{content:"2333";}} @media (min-device-width:2334px){#S:before{content:"2334";}} @media (min-device-width:2335px){#S:before{content:"2335";}} @media (min-device-width:2336px){#S:before{content:"2336";}} @media (min-device-width:2337px){#S:before{content:"2337";}} @media (min-device-width:2338px){#S:before{content:"2338";}} @media (min-device-width:2339px){#S:before{content:"2339";}} @media (min-device-width:2340px){#S:before{content:"2340";}} @media (min-device-width:2341px){#S:before{content:"2341";}} @media (min-device-width:2342px){#S:before{content:"2342";}} @media (min-device-width:2343px){#S:before{content:"2343";}} @media (min-device-width:2344px){#S:before{content:"2344";}} @media (min-device-width:2345px){#S:before{content:"2345";}} @media (min-device-width:2346px){#S:before{content:"2346";}} @media (min-device-width:2347px){#S:before{content:"2347";}} @media (min-device-width:2348px){#S:before{content:"2348";}} @media (min-device-width:2349px){#S:before{content:"2349";}} @media (min-device-width:2350px){#S:before{content:"2350";}} @media (min-device-width:2351px){#S:before{content:"2351";}} @media (min-device-width:2352px){#S:before{content:"2352";}} @media (min-device-width:2353px){#S:before{content:"2353";}} @media (min-device-width:2354px){#S:before{content:"2354";}} @media (min-device-width:2355px){#S:before{content:"2355";}} @media (min-device-width:2356px){#S:before{content:"2356";}} @media (min-device-width:2357px){#S:before{content:"2357";}} @media (min-device-width:2358px){#S:before{content:"2358";}} @media (min-device-width:2359px){#S:before{content:"2359";}} @media (min-device-width:2360px){#S:before{content:"2360";}} @media (min-device-width:2361px){#S:before{content:"2361";}} @media (min-device-width:2362px){#S:before{content:"2362";}} @media (min-device-width:2363px){#S:before{content:"2363";}} @media (min-device-width:2364px){#S:before{content:"2364";}} @media (min-device-width:2365px){#S:before{content:"2365";}} @media (min-device-width:2366px){#S:before{content:"2366";}} @media (min-device-width:2367px){#S:before{content:"2367";}} @media (min-device-width:2368px){#S:before{content:"2368";}} @media (min-device-width:2369px){#S:before{content:"2369";}} @media (min-device-width:2370px){#S:before{content:"2370";}} @media (min-device-width:2371px){#S:before{content:"2371";}} @media (min-device-width:2372px){#S:before{content:"2372";}} @media (min-device-width:2373px){#S:before{content:"2373";}} @media (min-device-width:2374px){#S:before{content:"2374";}} @media (min-device-width:2375px){#S:before{content:"2375";}} @media (min-device-width:2376px){#S:before{content:"2376";}} @media (min-device-width:2377px){#S:before{content:"2377";}} @media (min-device-width:2378px){#S:before{content:"2378";}} @media (min-device-width:2379px){#S:before{content:"2379";}} @media (min-device-width:2380px){#S:before{content:"2380";}} @media (min-device-width:2381px){#S:before{content:"2381";}} @media (min-device-width:2382px){#S:before{content:"2382";}} @media (min-device-width:2383px){#S:before{content:"2383";}} @media (min-device-width:2384px){#S:before{content:"2384";}} @media (min-device-width:2385px){#S:before{content:"2385";}} @media (min-device-width:2386px){#S:before{content:"2386";}} @media (min-device-width:2387px){#S:before{content:"2387";}} @media (min-device-width:2388px){#S:before{content:"2388";}} @media (min-device-width:2389px){#S:before{content:"2389";}} @media (min-device-width:2390px){#S:before{content:"2390";}} @media (min-device-width:2391px){#S:before{content:"2391";}} @media (min-device-width:2392px){#S:before{content:"2392";}} @media (min-device-width:2393px){#S:before{content:"2393";}} @media (min-device-width:2394px){#S:before{content:"2394";}} @media (min-device-width:2395px){#S:before{content:"2395";}} @media (min-device-width:2396px){#S:before{content:"2396";}} @media (min-device-width:2397px){#S:before{content:"2397";}} @media (min-device-width:2398px){#S:before{content:"2398";}} @media (min-device-width:2399px){#S:before{content:"2399";}} @media (min-device-width:2400px){#S:before{content:"2400";}} @media (min-device-width:2401px){#S:before{content:"2401";}} @media (min-device-width:2402px){#S:before{content:"2402";}} @media (min-device-width:2403px){#S:before{content:"2403";}} @media (min-device-width:2404px){#S:before{content:"2404";}} @media (min-device-width:2405px){#S:before{content:"2405";}} @media (min-device-width:2406px){#S:before{content:"2406";}} @media (min-device-width:2407px){#S:before{content:"2407";}} @media (min-device-width:2408px){#S:before{content:"2408";}} @media (min-device-width:2409px){#S:before{content:"2409";}} @media (min-device-width:2410px){#S:before{content:"2410";}} @media (min-device-width:2411px){#S:before{content:"2411";}} @media (min-device-width:2412px){#S:before{content:"2412";}} @media (min-device-width:2413px){#S:before{content:"2413";}} @media (min-device-width:2414px){#S:before{content:"2414";}} @media (min-device-width:2415px){#S:before{content:"2415";}} @media (min-device-width:2416px){#S:before{content:"2416";}} @media (min-device-width:2417px){#S:before{content:"2417";}} @media (min-device-width:2418px){#S:before{content:"2418";}} @media (min-device-width:2419px){#S:before{content:"2419";}} @media (min-device-width:2420px){#S:before{content:"2420";}} @media (min-device-width:2421px){#S:before{content:"2421";}} @media (min-device-width:2422px){#S:before{content:"2422";}} @media (min-device-width:2423px){#S:before{content:"2423";}} @media (min-device-width:2424px){#S:before{content:"2424";}} @media (min-device-width:2425px){#S:before{content:"2425";}} @media (min-device-width:2426px){#S:before{content:"2426";}} @media (min-device-width:2427px){#S:before{content:"2427";}} @media (min-device-width:2428px){#S:before{content:"2428";}} @media (min-device-width:2429px){#S:before{content:"2429";}} @media (min-device-width:2430px){#S:before{content:"2430";}} @media (min-device-width:2431px){#S:before{content:"2431";}} @media (min-device-width:2432px){#S:before{content:"2432";}} @media (min-device-width:2433px){#S:before{content:"2433";}} @media (min-device-width:2434px){#S:before{content:"2434";}} @media (min-device-width:2435px){#S:before{content:"2435";}} @media (min-device-width:2436px){#S:before{content:"2436";}} @media (min-device-width:2437px){#S:before{content:"2437";}} @media (min-device-width:2438px){#S:before{content:"2438";}} @media (min-device-width:2439px){#S:before{content:"2439";}} @media (min-device-width:2440px){#S:before{content:"2440";}} @media (min-device-width:2441px){#S:before{content:"2441";}} @media (min-device-width:2442px){#S:before{content:"2442";}} @media (min-device-width:2443px){#S:before{content:"2443";}} @media (min-device-width:2444px){#S:before{content:"2444";}} @media (min-device-width:2445px){#S:before{content:"2445";}} @media (min-device-width:2446px){#S:before{content:"2446";}} @media (min-device-width:2447px){#S:before{content:"2447";}} @media (min-device-width:2448px){#S:before{content:"2448";}} @media (min-device-width:2449px){#S:before{content:"2449";}} @media (min-device-width:2450px){#S:before{content:"2450";}} @media (min-device-width:2451px){#S:before{content:"2451";}} @media (min-device-width:2452px){#S:before{content:"2452";}} @media (min-device-width:2453px){#S:before{content:"2453";}} @media (min-device-width:2454px){#S:before{content:"2454";}} @media (min-device-width:2455px){#S:before{content:"2455";}} @media (min-device-width:2456px){#S:before{content:"2456";}} @media (min-device-width:2457px){#S:before{content:"2457";}} @media (min-device-width:2458px){#S:before{content:"2458";}} @media (min-device-width:2459px){#S:before{content:"2459";}} @media (min-device-width:2460px){#S:before{content:"2460";}} @media (min-device-width:2461px){#S:before{content:"2461";}} @media (min-device-width:2462px){#S:before{content:"2462";}} @media (min-device-width:2463px){#S:before{content:"2463";}} @media (min-device-width:2464px){#S:before{content:"2464";}} @media (min-device-width:2465px){#S:before{content:"2465";}} @media (min-device-width:2466px){#S:before{content:"2466";}} @media (min-device-width:2467px){#S:before{content:"2467";}} @media (min-device-width:2468px){#S:before{content:"2468";}} @media (min-device-width:2469px){#S:before{content:"2469";}} @media (min-device-width:2470px){#S:before{content:"2470";}} @media (min-device-width:2471px){#S:before{content:"2471";}} @media (min-device-width:2472px){#S:before{content:"2472";}} @media (min-device-width:2473px){#S:before{content:"2473";}} @media (min-device-width:2474px){#S:before{content:"2474";}} @media (min-device-width:2475px){#S:before{content:"2475";}} @media (min-device-width:2476px){#S:before{content:"2476";}} @media (min-device-width:2477px){#S:before{content:"2477";}} @media (min-device-width:2478px){#S:before{content:"2478";}} @media (min-device-width:2479px){#S:before{content:"2479";}} @media (min-device-width:2480px){#S:before{content:"2480";}} @media (min-device-width:2481px){#S:before{content:"2481";}} @media (min-device-width:2482px){#S:before{content:"2482";}} @media (min-device-width:2483px){#S:before{content:"2483";}} @media (min-device-width:2484px){#S:before{content:"2484";}} @media (min-device-width:2485px){#S:before{content:"2485";}} @media (min-device-width:2486px){#S:before{content:"2486";}} @media (min-device-width:2487px){#S:before{content:"2487";}} @media (min-device-width:2488px){#S:before{content:"2488";}} @media (min-device-width:2489px){#S:before{content:"2489";}} @media (min-device-width:2490px){#S:before{content:"2490";}} @media (min-device-width:2491px){#S:before{content:"2491";}} @media (min-device-width:2492px){#S:before{content:"2492";}} @media (min-device-width:2493px){#S:before{content:"2493";}} @media (min-device-width:2494px){#S:before{content:"2494";}} @media (min-device-width:2495px){#S:before{content:"2495";}} @media (min-device-width:2496px){#S:before{content:"2496";}} @media (min-device-width:2497px){#S:before{content:"2497";}} @media (min-device-width:2498px){#S:before{content:"2498";}} @media (min-device-width:2499px){#S:before{content:"2499";}} @media (min-device-width:2500px){#S:before{content:"2500";}} @media (min-device-width:2501px){#S:before{content:"2501";}} @media (min-device-width:2502px){#S:before{content:"2502";}} @media (min-device-width:2503px){#S:before{content:"2503";}} @media (min-device-width:2504px){#S:before{content:"2504";}} @media (min-device-width:2505px){#S:before{content:"2505";}} @media (min-device-width:2506px){#S:before{content:"2506";}} @media (min-device-width:2507px){#S:before{content:"2507";}} @media (min-device-width:2508px){#S:before{content:"2508";}} @media (min-device-width:2509px){#S:before{content:"2509";}} @media (min-device-width:2510px){#S:before{content:"2510";}} @media (min-device-width:2511px){#S:before{content:"2511";}} @media (min-device-width:2512px){#S:before{content:"2512";}} @media (min-device-width:2513px){#S:before{content:"2513";}} @media (min-device-width:2514px){#S:before{content:"2514";}} @media (min-device-width:2515px){#S:before{content:"2515";}} @media (min-device-width:2516px){#S:before{content:"2516";}} @media (min-device-width:2517px){#S:before{content:"2517";}} @media (min-device-width:2518px){#S:before{content:"2518";}} @media (min-device-width:2519px){#S:before{content:"2519";}} @media (min-device-width:2520px){#S:before{content:"2520";}} @media (min-device-width:2521px){#S:before{content:"2521";}} @media (min-device-width:2522px){#S:before{content:"2522";}} @media (min-device-width:2523px){#S:before{content:"2523";}} @media (min-device-width:2524px){#S:before{content:"2524";}} @media (min-device-width:2525px){#S:before{content:"2525";}} @media (min-device-width:2526px){#S:before{content:"2526";}} @media (min-device-width:2527px){#S:before{content:"2527";}} @media (min-device-width:2528px){#S:before{content:"2528";}} @media (min-device-width:2529px){#S:before{content:"2529";}} @media (min-device-width:2530px){#S:before{content:"2530";}} @media (min-device-width:2531px){#S:before{content:"2531";}} @media (min-device-width:2532px){#S:before{content:"2532";}} @media (min-device-width:2533px){#S:before{content:"2533";}} @media (min-device-width:2534px){#S:before{content:"2534";}} @media (min-device-width:2535px){#S:before{content:"2535";}} @media (min-device-width:2536px){#S:before{content:"2536";}} @media (min-device-width:2537px){#S:before{content:"2537";}} @media (min-device-width:2538px){#S:before{content:"2538";}} @media (min-device-width:2539px){#S:before{content:"2539";}} @media (min-device-width:2540px){#S:before{content:"2540";}} @media (min-device-width:2541px){#S:before{content:"2541";}} @media (min-device-width:2542px){#S:before{content:"2542";}} @media (min-device-width:2543px){#S:before{content:"2543";}} @media (min-device-width:2544px){#S:before{content:"2544";}} @media (min-device-width:2545px){#S:before{content:"2545";}} @media (min-device-width:2546px){#S:before{content:"2546";}} @media (min-device-width:2547px){#S:before{content:"2547";}} @media (min-device-width:2548px){#S:before{content:"2548";}} @media (min-device-width:2549px){#S:before{content:"2549";}} @media (min-device-width:2550px){#S:before{content:"2550";}} @media (min-device-width:2551px){#S:before{content:"2551";}} @media (min-device-width:2552px){#S:before{content:"2552";}} @media (min-device-width:2553px){#S:before{content:"2553";}} @media (min-device-width:2554px){#S:before{content:"2554";}} @media (min-device-width:2555px){#S:before{content:"2555";}} @media (min-device-width:2556px){#S:before{content:"2556";}} @media (min-device-width:2557px){#S:before{content:"2557";}} @media (min-device-width:2558px){#S:before{content:"2558";}} @media (min-device-width:2559px){#S:before{content:"2559";}} @media (min-device-width:2560px){#S:before{content:"2560";}} @media (min-device-width:2561px){#S:before{content:"";}} /* upper */ @media (min-device-height:399px){#S:after{content:"";}} /* lower */ @media (min-device-height:400px){#S:after{content:" x 400";}} @media (min-device-height:401px){#S:after{content:" x 401";}} @media (min-device-height:402px){#S:after{content:" x 402";}} @media (min-device-height:403px){#S:after{content:" x 403";}} @media (min-device-height:404px){#S:after{content:" x 404";}} @media (min-device-height:405px){#S:after{content:" x 405";}} @media (min-device-height:406px){#S:after{content:" x 406";}} @media (min-device-height:407px){#S:after{content:" x 407";}} @media (min-device-height:408px){#S:after{content:" x 408";}} @media (min-device-height:409px){#S:after{content:" x 409";}} @media (min-device-height:410px){#S:after{content:" x 410";}} @media (min-device-height:411px){#S:after{content:" x 411";}} @media (min-device-height:412px){#S:after{content:" x 412";}} @media (min-device-height:413px){#S:after{content:" x 413";}} @media (min-device-height:414px){#S:after{content:" x 414";}} @media (min-device-height:415px){#S:after{content:" x 415";}} @media (min-device-height:416px){#S:after{content:" x 416";}} @media (min-device-height:417px){#S:after{content:" x 417";}} @media (min-device-height:418px){#S:after{content:" x 418";}} @media (min-device-height:419px){#S:after{content:" x 419";}} @media (min-device-height:420px){#S:after{content:" x 420";}} @media (min-device-height:421px){#S:after{content:" x 421";}} @media (min-device-height:422px){#S:after{content:" x 422";}} @media (min-device-height:423px){#S:after{content:" x 423";}} @media (min-device-height:424px){#S:after{content:" x 424";}} @media (min-device-height:425px){#S:after{content:" x 425";}} @media (min-device-height:426px){#S:after{content:" x 426";}} @media (min-device-height:427px){#S:after{content:" x 427";}} @media (min-device-height:428px){#S:after{content:" x 428";}} @media (min-device-height:429px){#S:after{content:" x 429";}} @media (min-device-height:430px){#S:after{content:" x 430";}} @media (min-device-height:431px){#S:after{content:" x 431";}} @media (min-device-height:432px){#S:after{content:" x 432";}} @media (min-device-height:433px){#S:after{content:" x 433";}} @media (min-device-height:434px){#S:after{content:" x 434";}} @media (min-device-height:435px){#S:after{content:" x 435";}} @media (min-device-height:436px){#S:after{content:" x 436";}} @media (min-device-height:437px){#S:after{content:" x 437";}} @media (min-device-height:438px){#S:after{content:" x 438";}} @media (min-device-height:439px){#S:after{content:" x 439";}} @media (min-device-height:440px){#S:after{content:" x 440";}} @media (min-device-height:441px){#S:after{content:" x 441";}} @media (min-device-height:442px){#S:after{content:" x 442";}} @media (min-device-height:443px){#S:after{content:" x 443";}} @media (min-device-height:444px){#S:after{content:" x 444";}} @media (min-device-height:445px){#S:after{content:" x 445";}} @media (min-device-height:446px){#S:after{content:" x 446";}} @media (min-device-height:447px){#S:after{content:" x 447";}} @media (min-device-height:448px){#S:after{content:" x 448";}} @media (min-device-height:449px){#S:after{content:" x 449";}} @media (min-device-height:450px){#S:after{content:" x 450";}} @media (min-device-height:451px){#S:after{content:" x 451";}} @media (min-device-height:452px){#S:after{content:" x 452";}} @media (min-device-height:453px){#S:after{content:" x 453";}} @media (min-device-height:454px){#S:after{content:" x 454";}} @media (min-device-height:455px){#S:after{content:" x 455";}} @media (min-device-height:456px){#S:after{content:" x 456";}} @media (min-device-height:457px){#S:after{content:" x 457";}} @media (min-device-height:458px){#S:after{content:" x 458";}} @media (min-device-height:459px){#S:after{content:" x 459";}} @media (min-device-height:460px){#S:after{content:" x 460";}} @media (min-device-height:461px){#S:after{content:" x 461";}} @media (min-device-height:462px){#S:after{content:" x 462";}} @media (min-device-height:463px){#S:after{content:" x 463";}} @media (min-device-height:464px){#S:after{content:" x 464";}} @media (min-device-height:465px){#S:after{content:" x 465";}} @media (min-device-height:466px){#S:after{content:" x 466";}} @media (min-device-height:467px){#S:after{content:" x 467";}} @media (min-device-height:468px){#S:after{content:" x 468";}} @media (min-device-height:469px){#S:after{content:" x 469";}} @media (min-device-height:470px){#S:after{content:" x 470";}} @media (min-device-height:471px){#S:after{content:" x 471";}} @media (min-device-height:472px){#S:after{content:" x 472";}} @media (min-device-height:473px){#S:after{content:" x 473";}} @media (min-device-height:474px){#S:after{content:" x 474";}} @media (min-device-height:475px){#S:after{content:" x 475";}} @media (min-device-height:476px){#S:after{content:" x 476";}} @media (min-device-height:477px){#S:after{content:" x 477";}} @media (min-device-height:478px){#S:after{content:" x 478";}} @media (min-device-height:479px){#S:after{content:" x 479";}} @media (min-device-height:480px){#S:after{content:" x 480";}} @media (min-device-height:481px){#S:after{content:" x 481";}} @media (min-device-height:482px){#S:after{content:" x 482";}} @media (min-device-height:483px){#S:after{content:" x 483";}} @media (min-device-height:484px){#S:after{content:" x 484";}} @media (min-device-height:485px){#S:after{content:" x 485";}} @media (min-device-height:486px){#S:after{content:" x 486";}} @media (min-device-height:487px){#S:after{content:" x 487";}} @media (min-device-height:488px){#S:after{content:" x 488";}} @media (min-device-height:489px){#S:after{content:" x 489";}} @media (min-device-height:490px){#S:after{content:" x 490";}} @media (min-device-height:491px){#S:after{content:" x 491";}} @media (min-device-height:492px){#S:after{content:" x 492";}} @media (min-device-height:493px){#S:after{content:" x 493";}} @media (min-device-height:494px){#S:after{content:" x 494";}} @media (min-device-height:495px){#S:after{content:" x 495";}} @media (min-device-height:496px){#S:after{content:" x 496";}} @media (min-device-height:497px){#S:after{content:" x 497";}} @media (min-device-height:498px){#S:after{content:" x 498";}} @media (min-device-height:499px){#S:after{content:" x 499";}} @media (min-device-height:500px){#S:after{content:" x 500";}} @media (min-device-height:501px){#S:after{content:" x 501";}} @media (min-device-height:502px){#S:after{content:" x 502";}} @media (min-device-height:503px){#S:after{content:" x 503";}} @media (min-device-height:504px){#S:after{content:" x 504";}} @media (min-device-height:505px){#S:after{content:" x 505";}} @media (min-device-height:506px){#S:after{content:" x 506";}} @media (min-device-height:507px){#S:after{content:" x 507";}} @media (min-device-height:508px){#S:after{content:" x 508";}} @media (min-device-height:509px){#S:after{content:" x 509";}} @media (min-device-height:510px){#S:after{content:" x 510";}} @media (min-device-height:511px){#S:after{content:" x 511";}} @media (min-device-height:512px){#S:after{content:" x 512";}} @media (min-device-height:513px){#S:after{content:" x 513";}} @media (min-device-height:514px){#S:after{content:" x 514";}} @media (min-device-height:515px){#S:after{content:" x 515";}} @media (min-device-height:516px){#S:after{content:" x 516";}} @media (min-device-height:517px){#S:after{content:" x 517";}} @media (min-device-height:518px){#S:after{content:" x 518";}} @media (min-device-height:519px){#S:after{content:" x 519";}} @media (min-device-height:520px){#S:after{content:" x 520";}} @media (min-device-height:521px){#S:after{content:" x 521";}} @media (min-device-height:522px){#S:after{content:" x 522";}} @media (min-device-height:523px){#S:after{content:" x 523";}} @media (min-device-height:524px){#S:after{content:" x 524";}} @media (min-device-height:525px){#S:after{content:" x 525";}} @media (min-device-height:526px){#S:after{content:" x 526";}} @media (min-device-height:527px){#S:after{content:" x 527";}} @media (min-device-height:528px){#S:after{content:" x 528";}} @media (min-device-height:529px){#S:after{content:" x 529";}} @media (min-device-height:530px){#S:after{content:" x 530";}} @media (min-device-height:531px){#S:after{content:" x 531";}} @media (min-device-height:532px){#S:after{content:" x 532";}} @media (min-device-height:533px){#S:after{content:" x 533";}} @media (min-device-height:534px){#S:after{content:" x 534";}} @media (min-device-height:535px){#S:after{content:" x 535";}} @media (min-device-height:536px){#S:after{content:" x 536";}} @media (min-device-height:537px){#S:after{content:" x 537";}} @media (min-device-height:538px){#S:after{content:" x 538";}} @media (min-device-height:539px){#S:after{content:" x 539";}} @media (min-device-height:540px){#S:after{content:" x 540";}} @media (min-device-height:541px){#S:after{content:" x 541";}} @media (min-device-height:542px){#S:after{content:" x 542";}} @media (min-device-height:543px){#S:after{content:" x 543";}} @media (min-device-height:544px){#S:after{content:" x 544";}} @media (min-device-height:545px){#S:after{content:" x 545";}} @media (min-device-height:546px){#S:after{content:" x 546";}} @media (min-device-height:547px){#S:after{content:" x 547";}} @media (min-device-height:548px){#S:after{content:" x 548";}} @media (min-device-height:549px){#S:after{content:" x 549";}} @media (min-device-height:550px){#S:after{content:" x 550";}} @media (min-device-height:551px){#S:after{content:" x 551";}} @media (min-device-height:552px){#S:after{content:" x 552";}} @media (min-device-height:553px){#S:after{content:" x 553";}} @media (min-device-height:554px){#S:after{content:" x 554";}} @media (min-device-height:555px){#S:after{content:" x 555";}} @media (min-device-height:556px){#S:after{content:" x 556";}} @media (min-device-height:557px){#S:after{content:" x 557";}} @media (min-device-height:558px){#S:after{content:" x 558";}} @media (min-device-height:559px){#S:after{content:" x 559";}} @media (min-device-height:560px){#S:after{content:" x 560";}} @media (min-device-height:561px){#S:after{content:" x 561";}} @media (min-device-height:562px){#S:after{content:" x 562";}} @media (min-device-height:563px){#S:after{content:" x 563";}} @media (min-device-height:564px){#S:after{content:" x 564";}} @media (min-device-height:565px){#S:after{content:" x 565";}} @media (min-device-height:566px){#S:after{content:" x 566";}} @media (min-device-height:567px){#S:after{content:" x 567";}} @media (min-device-height:568px){#S:after{content:" x 568";}} @media (min-device-height:569px){#S:after{content:" x 569";}} @media (min-device-height:570px){#S:after{content:" x 570";}} @media (min-device-height:571px){#S:after{content:" x 571";}} @media (min-device-height:572px){#S:after{content:" x 572";}} @media (min-device-height:573px){#S:after{content:" x 573";}} @media (min-device-height:574px){#S:after{content:" x 574";}} @media (min-device-height:575px){#S:after{content:" x 575";}} @media (min-device-height:576px){#S:after{content:" x 576";}} @media (min-device-height:577px){#S:after{content:" x 577";}} @media (min-device-height:578px){#S:after{content:" x 578";}} @media (min-device-height:579px){#S:after{content:" x 579";}} @media (min-device-height:580px){#S:after{content:" x 580";}} @media (min-device-height:581px){#S:after{content:" x 581";}} @media (min-device-height:582px){#S:after{content:" x 582";}} @media (min-device-height:583px){#S:after{content:" x 583";}} @media (min-device-height:584px){#S:after{content:" x 584";}} @media (min-device-height:585px){#S:after{content:" x 585";}} @media (min-device-height:586px){#S:after{content:" x 586";}} @media (min-device-height:587px){#S:after{content:" x 587";}} @media (min-device-height:588px){#S:after{content:" x 588";}} @media (min-device-height:589px){#S:after{content:" x 589";}} @media (min-device-height:590px){#S:after{content:" x 590";}} @media (min-device-height:591px){#S:after{content:" x 591";}} @media (min-device-height:592px){#S:after{content:" x 592";}} @media (min-device-height:593px){#S:after{content:" x 593";}} @media (min-device-height:594px){#S:after{content:" x 594";}} @media (min-device-height:595px){#S:after{content:" x 595";}} @media (min-device-height:596px){#S:after{content:" x 596";}} @media (min-device-height:597px){#S:after{content:" x 597";}} @media (min-device-height:598px){#S:after{content:" x 598";}} @media (min-device-height:599px){#S:after{content:" x 599";}} @media (min-device-height:600px){#S:after{content:" x 600";}} @media (min-device-height:601px){#S:after{content:" x 601";}} @media (min-device-height:602px){#S:after{content:" x 602";}} @media (min-device-height:603px){#S:after{content:" x 603";}} @media (min-device-height:604px){#S:after{content:" x 604";}} @media (min-device-height:605px){#S:after{content:" x 605";}} @media (min-device-height:606px){#S:after{content:" x 606";}} @media (min-device-height:607px){#S:after{content:" x 607";}} @media (min-device-height:608px){#S:after{content:" x 608";}} @media (min-device-height:609px){#S:after{content:" x 609";}} @media (min-device-height:610px){#S:after{content:" x 610";}} @media (min-device-height:611px){#S:after{content:" x 611";}} @media (min-device-height:612px){#S:after{content:" x 612";}} @media (min-device-height:613px){#S:after{content:" x 613";}} @media (min-device-height:614px){#S:after{content:" x 614";}} @media (min-device-height:615px){#S:after{content:" x 615";}} @media (min-device-height:616px){#S:after{content:" x 616";}} @media (min-device-height:617px){#S:after{content:" x 617";}} @media (min-device-height:618px){#S:after{content:" x 618";}} @media (min-device-height:619px){#S:after{content:" x 619";}} @media (min-device-height:620px){#S:after{content:" x 620";}} @media (min-device-height:621px){#S:after{content:" x 621";}} @media (min-device-height:622px){#S:after{content:" x 622";}} @media (min-device-height:623px){#S:after{content:" x 623";}} @media (min-device-height:624px){#S:after{content:" x 624";}} @media (min-device-height:625px){#S:after{content:" x 625";}} @media (min-device-height:626px){#S:after{content:" x 626";}} @media (min-device-height:627px){#S:after{content:" x 627";}} @media (min-device-height:628px){#S:after{content:" x 628";}} @media (min-device-height:629px){#S:after{content:" x 629";}} @media (min-device-height:630px){#S:after{content:" x 630";}} @media (min-device-height:631px){#S:after{content:" x 631";}} @media (min-device-height:632px){#S:after{content:" x 632";}} @media (min-device-height:633px){#S:after{content:" x 633";}} @media (min-device-height:634px){#S:after{content:" x 634";}} @media (min-device-height:635px){#S:after{content:" x 635";}} @media (min-device-height:636px){#S:after{content:" x 636";}} @media (min-device-height:637px){#S:after{content:" x 637";}} @media (min-device-height:638px){#S:after{content:" x 638";}} @media (min-device-height:639px){#S:after{content:" x 639";}} @media (min-device-height:640px){#S:after{content:" x 640";}} @media (min-device-height:641px){#S:after{content:" x 641";}} @media (min-device-height:642px){#S:after{content:" x 642";}} @media (min-device-height:643px){#S:after{content:" x 643";}} @media (min-device-height:644px){#S:after{content:" x 644";}} @media (min-device-height:645px){#S:after{content:" x 645";}} @media (min-device-height:646px){#S:after{content:" x 646";}} @media (min-device-height:647px){#S:after{content:" x 647";}} @media (min-device-height:648px){#S:after{content:" x 648";}} @media (min-device-height:649px){#S:after{content:" x 649";}} @media (min-device-height:650px){#S:after{content:" x 650";}} @media (min-device-height:651px){#S:after{content:" x 651";}} @media (min-device-height:652px){#S:after{content:" x 652";}} @media (min-device-height:653px){#S:after{content:" x 653";}} @media (min-device-height:654px){#S:after{content:" x 654";}} @media (min-device-height:655px){#S:after{content:" x 655";}} @media (min-device-height:656px){#S:after{content:" x 656";}} @media (min-device-height:657px){#S:after{content:" x 657";}} @media (min-device-height:658px){#S:after{content:" x 658";}} @media (min-device-height:659px){#S:after{content:" x 659";}} @media (min-device-height:660px){#S:after{content:" x 660";}} @media (min-device-height:661px){#S:after{content:" x 661";}} @media (min-device-height:662px){#S:after{content:" x 662";}} @media (min-device-height:663px){#S:after{content:" x 663";}} @media (min-device-height:664px){#S:after{content:" x 664";}} @media (min-device-height:665px){#S:after{content:" x 665";}} @media (min-device-height:666px){#S:after{content:" x 666";}} @media (min-device-height:667px){#S:after{content:" x 667";}} @media (min-device-height:668px){#S:after{content:" x 668";}} @media (min-device-height:669px){#S:after{content:" x 669";}} @media (min-device-height:670px){#S:after{content:" x 670";}} @media (min-device-height:671px){#S:after{content:" x 671";}} @media (min-device-height:672px){#S:after{content:" x 672";}} @media (min-device-height:673px){#S:after{content:" x 673";}} @media (min-device-height:674px){#S:after{content:" x 674";}} @media (min-device-height:675px){#S:after{content:" x 675";}} @media (min-device-height:676px){#S:after{content:" x 676";}} @media (min-device-height:677px){#S:after{content:" x 677";}} @media (min-device-height:678px){#S:after{content:" x 678";}} @media (min-device-height:679px){#S:after{content:" x 679";}} @media (min-device-height:680px){#S:after{content:" x 680";}} @media (min-device-height:681px){#S:after{content:" x 681";}} @media (min-device-height:682px){#S:after{content:" x 682";}} @media (min-device-height:683px){#S:after{content:" x 683";}} @media (min-device-height:684px){#S:after{content:" x 684";}} @media (min-device-height:685px){#S:after{content:" x 685";}} @media (min-device-height:686px){#S:after{content:" x 686";}} @media (min-device-height:687px){#S:after{content:" x 687";}} @media (min-device-height:688px){#S:after{content:" x 688";}} @media (min-device-height:689px){#S:after{content:" x 689";}} @media (min-device-height:690px){#S:after{content:" x 690";}} @media (min-device-height:691px){#S:after{content:" x 691";}} @media (min-device-height:692px){#S:after{content:" x 692";}} @media (min-device-height:693px){#S:after{content:" x 693";}} @media (min-device-height:694px){#S:after{content:" x 694";}} @media (min-device-height:695px){#S:after{content:" x 695";}} @media (min-device-height:696px){#S:after{content:" x 696";}} @media (min-device-height:697px){#S:after{content:" x 697";}} @media (min-device-height:698px){#S:after{content:" x 698";}} @media (min-device-height:699px){#S:after{content:" x 699";}} @media (min-device-height:700px){#S:after{content:" x 700";}} @media (min-device-height:701px){#S:after{content:" x 701";}} @media (min-device-height:702px){#S:after{content:" x 702";}} @media (min-device-height:703px){#S:after{content:" x 703";}} @media (min-device-height:704px){#S:after{content:" x 704";}} @media (min-device-height:705px){#S:after{content:" x 705";}} @media (min-device-height:706px){#S:after{content:" x 706";}} @media (min-device-height:707px){#S:after{content:" x 707";}} @media (min-device-height:708px){#S:after{content:" x 708";}} @media (min-device-height:709px){#S:after{content:" x 709";}} @media (min-device-height:710px){#S:after{content:" x 710";}} @media (min-device-height:711px){#S:after{content:" x 711";}} @media (min-device-height:712px){#S:after{content:" x 712";}} @media (min-device-height:713px){#S:after{content:" x 713";}} @media (min-device-height:714px){#S:after{content:" x 714";}} @media (min-device-height:715px){#S:after{content:" x 715";}} @media (min-device-height:716px){#S:after{content:" x 716";}} @media (min-device-height:717px){#S:after{content:" x 717";}} @media (min-device-height:718px){#S:after{content:" x 718";}} @media (min-device-height:719px){#S:after{content:" x 719";}} @media (min-device-height:720px){#S:after{content:" x 720";}} @media (min-device-height:721px){#S:after{content:" x 721";}} @media (min-device-height:722px){#S:after{content:" x 722";}} @media (min-device-height:723px){#S:after{content:" x 723";}} @media (min-device-height:724px){#S:after{content:" x 724";}} @media (min-device-height:725px){#S:after{content:" x 725";}} @media (min-device-height:726px){#S:after{content:" x 726";}} @media (min-device-height:727px){#S:after{content:" x 727";}} @media (min-device-height:728px){#S:after{content:" x 728";}} @media (min-device-height:729px){#S:after{content:" x 729";}} @media (min-device-height:730px){#S:after{content:" x 730";}} @media (min-device-height:731px){#S:after{content:" x 731";}} @media (min-device-height:732px){#S:after{content:" x 732";}} @media (min-device-height:733px){#S:after{content:" x 733";}} @media (min-device-height:734px){#S:after{content:" x 734";}} @media (min-device-height:735px){#S:after{content:" x 735";}} @media (min-device-height:736px){#S:after{content:" x 736";}} @media (min-device-height:737px){#S:after{content:" x 737";}} @media (min-device-height:738px){#S:after{content:" x 738";}} @media (min-device-height:739px){#S:after{content:" x 739";}} @media (min-device-height:740px){#S:after{content:" x 740";}} @media (min-device-height:741px){#S:after{content:" x 741";}} @media (min-device-height:742px){#S:after{content:" x 742";}} @media (min-device-height:743px){#S:after{content:" x 743";}} @media (min-device-height:744px){#S:after{content:" x 744";}} @media (min-device-height:745px){#S:after{content:" x 745";}} @media (min-device-height:746px){#S:after{content:" x 746";}} @media (min-device-height:747px){#S:after{content:" x 747";}} @media (min-device-height:748px){#S:after{content:" x 748";}} @media (min-device-height:749px){#S:after{content:" x 749";}} @media (min-device-height:750px){#S:after{content:" x 750";}} @media (min-device-height:751px){#S:after{content:" x 751";}} @media (min-device-height:752px){#S:after{content:" x 752";}} @media (min-device-height:753px){#S:after{content:" x 753";}} @media (min-device-height:754px){#S:after{content:" x 754";}} @media (min-device-height:755px){#S:after{content:" x 755";}} @media (min-device-height:756px){#S:after{content:" x 756";}} @media (min-device-height:757px){#S:after{content:" x 757";}} @media (min-device-height:758px){#S:after{content:" x 758";}} @media (min-device-height:759px){#S:after{content:" x 759";}} @media (min-device-height:760px){#S:after{content:" x 760";}} @media (min-device-height:761px){#S:after{content:" x 761";}} @media (min-device-height:762px){#S:after{content:" x 762";}} @media (min-device-height:763px){#S:after{content:" x 763";}} @media (min-device-height:764px){#S:after{content:" x 764";}} @media (min-device-height:765px){#S:after{content:" x 765";}} @media (min-device-height:766px){#S:after{content:" x 766";}} @media (min-device-height:767px){#S:after{content:" x 767";}} @media (min-device-height:768px){#S:after{content:" x 768";}} @media (min-device-height:769px){#S:after{content:" x 769";}} @media (min-device-height:770px){#S:after{content:" x 770";}} @media (min-device-height:771px){#S:after{content:" x 771";}} @media (min-device-height:772px){#S:after{content:" x 772";}} @media (min-device-height:773px){#S:after{content:" x 773";}} @media (min-device-height:774px){#S:after{content:" x 774";}} @media (min-device-height:775px){#S:after{content:" x 775";}} @media (min-device-height:776px){#S:after{content:" x 776";}} @media (min-device-height:777px){#S:after{content:" x 777";}} @media (min-device-height:778px){#S:after{content:" x 778";}} @media (min-device-height:779px){#S:after{content:" x 779";}} @media (min-device-height:780px){#S:after{content:" x 780";}} @media (min-device-height:781px){#S:after{content:" x 781";}} @media (min-device-height:782px){#S:after{content:" x 782";}} @media (min-device-height:783px){#S:after{content:" x 783";}} @media (min-device-height:784px){#S:after{content:" x 784";}} @media (min-device-height:785px){#S:after{content:" x 785";}} @media (min-device-height:786px){#S:after{content:" x 786";}} @media (min-device-height:787px){#S:after{content:" x 787";}} @media (min-device-height:788px){#S:after{content:" x 788";}} @media (min-device-height:789px){#S:after{content:" x 789";}} @media (min-device-height:790px){#S:after{content:" x 790";}} @media (min-device-height:791px){#S:after{content:" x 791";}} @media (min-device-height:792px){#S:after{content:" x 792";}} @media (min-device-height:793px){#S:after{content:" x 793";}} @media (min-device-height:794px){#S:after{content:" x 794";}} @media (min-device-height:795px){#S:after{content:" x 795";}} @media (min-device-height:796px){#S:after{content:" x 796";}} @media (min-device-height:797px){#S:after{content:" x 797";}} @media (min-device-height:798px){#S:after{content:" x 798";}} @media (min-device-height:799px){#S:after{content:" x 799";}} @media (min-device-height:800px){#S:after{content:" x 800";}} @media (min-device-height:801px){#S:after{content:" x 801";}} @media (min-device-height:802px){#S:after{content:" x 802";}} @media (min-device-height:803px){#S:after{content:" x 803";}} @media (min-device-height:804px){#S:after{content:" x 804";}} @media (min-device-height:805px){#S:after{content:" x 805";}} @media (min-device-height:806px){#S:after{content:" x 806";}} @media (min-device-height:807px){#S:after{content:" x 807";}} @media (min-device-height:808px){#S:after{content:" x 808";}} @media (min-device-height:809px){#S:after{content:" x 809";}} @media (min-device-height:810px){#S:after{content:" x 810";}} @media (min-device-height:811px){#S:after{content:" x 811";}} @media (min-device-height:812px){#S:after{content:" x 812";}} @media (min-device-height:813px){#S:after{content:" x 813";}} @media (min-device-height:814px){#S:after{content:" x 814";}} @media (min-device-height:815px){#S:after{content:" x 815";}} @media (min-device-height:816px){#S:after{content:" x 816";}} @media (min-device-height:817px){#S:after{content:" x 817";}} @media (min-device-height:818px){#S:after{content:" x 818";}} @media (min-device-height:819px){#S:after{content:" x 819";}} @media (min-device-height:820px){#S:after{content:" x 820";}} @media (min-device-height:821px){#S:after{content:" x 821";}} @media (min-device-height:822px){#S:after{content:" x 822";}} @media (min-device-height:823px){#S:after{content:" x 823";}} @media (min-device-height:824px){#S:after{content:" x 824";}} @media (min-device-height:825px){#S:after{content:" x 825";}} @media (min-device-height:826px){#S:after{content:" x 826";}} @media (min-device-height:827px){#S:after{content:" x 827";}} @media (min-device-height:828px){#S:after{content:" x 828";}} @media (min-device-height:829px){#S:after{content:" x 829";}} @media (min-device-height:830px){#S:after{content:" x 830";}} @media (min-device-height:831px){#S:after{content:" x 831";}} @media (min-device-height:832px){#S:after{content:" x 832";}} @media (min-device-height:833px){#S:after{content:" x 833";}} @media (min-device-height:834px){#S:after{content:" x 834";}} @media (min-device-height:835px){#S:after{content:" x 835";}} @media (min-device-height:836px){#S:after{content:" x 836";}} @media (min-device-height:837px){#S:after{content:" x 837";}} @media (min-device-height:838px){#S:after{content:" x 838";}} @media (min-device-height:839px){#S:after{content:" x 839";}} @media (min-device-height:840px){#S:after{content:" x 840";}} @media (min-device-height:841px){#S:after{content:" x 841";}} @media (min-device-height:842px){#S:after{content:" x 842";}} @media (min-device-height:843px){#S:after{content:" x 843";}} @media (min-device-height:844px){#S:after{content:" x 844";}} @media (min-device-height:845px){#S:after{content:" x 845";}} @media (min-device-height:846px){#S:after{content:" x 846";}} @media (min-device-height:847px){#S:after{content:" x 847";}} @media (min-device-height:848px){#S:after{content:" x 848";}} @media (min-device-height:849px){#S:after{content:" x 849";}} @media (min-device-height:850px){#S:after{content:" x 850";}} @media (min-device-height:851px){#S:after{content:" x 851";}} @media (min-device-height:852px){#S:after{content:" x 852";}} @media (min-device-height:853px){#S:after{content:" x 853";}} @media (min-device-height:854px){#S:after{content:" x 854";}} @media (min-device-height:855px){#S:after{content:" x 855";}} @media (min-device-height:856px){#S:after{content:" x 856";}} @media (min-device-height:857px){#S:after{content:" x 857";}} @media (min-device-height:858px){#S:after{content:" x 858";}} @media (min-device-height:859px){#S:after{content:" x 859";}} @media (min-device-height:860px){#S:after{content:" x 860";}} @media (min-device-height:861px){#S:after{content:" x 861";}} @media (min-device-height:862px){#S:after{content:" x 862";}} @media (min-device-height:863px){#S:after{content:" x 863";}} @media (min-device-height:864px){#S:after{content:" x 864";}} @media (min-device-height:865px){#S:after{content:" x 865";}} @media (min-device-height:866px){#S:after{content:" x 866";}} @media (min-device-height:867px){#S:after{content:" x 867";}} @media (min-device-height:868px){#S:after{content:" x 868";}} @media (min-device-height:869px){#S:after{content:" x 869";}} @media (min-device-height:870px){#S:after{content:" x 870";}} @media (min-device-height:871px){#S:after{content:" x 871";}} @media (min-device-height:872px){#S:after{content:" x 872";}} @media (min-device-height:873px){#S:after{content:" x 873";}} @media (min-device-height:874px){#S:after{content:" x 874";}} @media (min-device-height:875px){#S:after{content:" x 875";}} @media (min-device-height:876px){#S:after{content:" x 876";}} @media (min-device-height:877px){#S:after{content:" x 877";}} @media (min-device-height:878px){#S:after{content:" x 878";}} @media (min-device-height:879px){#S:after{content:" x 879";}} @media (min-device-height:880px){#S:after{content:" x 880";}} @media (min-device-height:881px){#S:after{content:" x 881";}} @media (min-device-height:882px){#S:after{content:" x 882";}} @media (min-device-height:883px){#S:after{content:" x 883";}} @media (min-device-height:884px){#S:after{content:" x 884";}} @media (min-device-height:885px){#S:after{content:" x 885";}} @media (min-device-height:886px){#S:after{content:" x 886";}} @media (min-device-height:887px){#S:after{content:" x 887";}} @media (min-device-height:888px){#S:after{content:" x 888";}} @media (min-device-height:889px){#S:after{content:" x 889";}} @media (min-device-height:890px){#S:after{content:" x 890";}} @media (min-device-height:891px){#S:after{content:" x 891";}} @media (min-device-height:892px){#S:after{content:" x 892";}} @media (min-device-height:893px){#S:after{content:" x 893";}} @media (min-device-height:894px){#S:after{content:" x 894";}} @media (min-device-height:895px){#S:after{content:" x 895";}} @media (min-device-height:896px){#S:after{content:" x 896";}} @media (min-device-height:897px){#S:after{content:" x 897";}} @media (min-device-height:898px){#S:after{content:" x 898";}} @media (min-device-height:899px){#S:after{content:" x 899";}} @media (min-device-height:900px){#S:after{content:" x 900";}} @media (min-device-height:901px){#S:after{content:" x 901";}} @media (min-device-height:902px){#S:after{content:" x 902";}} @media (min-device-height:903px){#S:after{content:" x 903";}} @media (min-device-height:904px){#S:after{content:" x 904";}} @media (min-device-height:905px){#S:after{content:" x 905";}} @media (min-device-height:906px){#S:after{content:" x 906";}} @media (min-device-height:907px){#S:after{content:" x 907";}} @media (min-device-height:908px){#S:after{content:" x 908";}} @media (min-device-height:909px){#S:after{content:" x 909";}} @media (min-device-height:910px){#S:after{content:" x 910";}} @media (min-device-height:911px){#S:after{content:" x 911";}} @media (min-device-height:912px){#S:after{content:" x 912";}} @media (min-device-height:913px){#S:after{content:" x 913";}} @media (min-device-height:914px){#S:after{content:" x 914";}} @media (min-device-height:915px){#S:after{content:" x 915";}} @media (min-device-height:916px){#S:after{content:" x 916";}} @media (min-device-height:917px){#S:after{content:" x 917";}} @media (min-device-height:918px){#S:after{content:" x 918";}} @media (min-device-height:919px){#S:after{content:" x 919";}} @media (min-device-height:920px){#S:after{content:" x 920";}} @media (min-device-height:921px){#S:after{content:" x 921";}} @media (min-device-height:922px){#S:after{content:" x 922";}} @media (min-device-height:923px){#S:after{content:" x 923";}} @media (min-device-height:924px){#S:after{content:" x 924";}} @media (min-device-height:925px){#S:after{content:" x 925";}} @media (min-device-height:926px){#S:after{content:" x 926";}} @media (min-device-height:927px){#S:after{content:" x 927";}} @media (min-device-height:928px){#S:after{content:" x 928";}} @media (min-device-height:929px){#S:after{content:" x 929";}} @media (min-device-height:930px){#S:after{content:" x 930";}} @media (min-device-height:931px){#S:after{content:" x 931";}} @media (min-device-height:932px){#S:after{content:" x 932";}} @media (min-device-height:933px){#S:after{content:" x 933";}} @media (min-device-height:934px){#S:after{content:" x 934";}} @media (min-device-height:935px){#S:after{content:" x 935";}} @media (min-device-height:936px){#S:after{content:" x 936";}} @media (min-device-height:937px){#S:after{content:" x 937";}} @media (min-device-height:938px){#S:after{content:" x 938";}} @media (min-device-height:939px){#S:after{content:" x 939";}} @media (min-device-height:940px){#S:after{content:" x 940";}} @media (min-device-height:941px){#S:after{content:" x 941";}} @media (min-device-height:942px){#S:after{content:" x 942";}} @media (min-device-height:943px){#S:after{content:" x 943";}} @media (min-device-height:944px){#S:after{content:" x 944";}} @media (min-device-height:945px){#S:after{content:" x 945";}} @media (min-device-height:946px){#S:after{content:" x 946";}} @media (min-device-height:947px){#S:after{content:" x 947";}} @media (min-device-height:948px){#S:after{content:" x 948";}} @media (min-device-height:949px){#S:after{content:" x 949";}} @media (min-device-height:950px){#S:after{content:" x 950";}} @media (min-device-height:951px){#S:after{content:" x 951";}} @media (min-device-height:952px){#S:after{content:" x 952";}} @media (min-device-height:953px){#S:after{content:" x 953";}} @media (min-device-height:954px){#S:after{content:" x 954";}} @media (min-device-height:955px){#S:after{content:" x 955";}} @media (min-device-height:956px){#S:after{content:" x 956";}} @media (min-device-height:957px){#S:after{content:" x 957";}} @media (min-device-height:958px){#S:after{content:" x 958";}} @media (min-device-height:959px){#S:after{content:" x 959";}} @media (min-device-height:960px){#S:after{content:" x 960";}} @media (min-device-height:961px){#S:after{content:" x 961";}} @media (min-device-height:962px){#S:after{content:" x 962";}} @media (min-device-height:963px){#S:after{content:" x 963";}} @media (min-device-height:964px){#S:after{content:" x 964";}} @media (min-device-height:965px){#S:after{content:" x 965";}} @media (min-device-height:966px){#S:after{content:" x 966";}} @media (min-device-height:967px){#S:after{content:" x 967";}} @media (min-device-height:968px){#S:after{content:" x 968";}} @media (min-device-height:969px){#S:after{content:" x 969";}} @media (min-device-height:970px){#S:after{content:" x 970";}} @media (min-device-height:971px){#S:after{content:" x 971";}} @media (min-device-height:972px){#S:after{content:" x 972";}} @media (min-device-height:973px){#S:after{content:" x 973";}} @media (min-device-height:974px){#S:after{content:" x 974";}} @media (min-device-height:975px){#S:after{content:" x 975";}} @media (min-device-height:976px){#S:after{content:" x 976";}} @media (min-device-height:977px){#S:after{content:" x 977";}} @media (min-device-height:978px){#S:after{content:" x 978";}} @media (min-device-height:979px){#S:after{content:" x 979";}} @media (min-device-height:980px){#S:after{content:" x 980";}} @media (min-device-height:981px){#S:after{content:" x 981";}} @media (min-device-height:982px){#S:after{content:" x 982";}} @media (min-device-height:983px){#S:after{content:" x 983";}} @media (min-device-height:984px){#S:after{content:" x 984";}} @media (min-device-height:985px){#S:after{content:" x 985";}} @media (min-device-height:986px){#S:after{content:" x 986";}} @media (min-device-height:987px){#S:after{content:" x 987";}} @media (min-device-height:988px){#S:after{content:" x 988";}} @media (min-device-height:989px){#S:after{content:" x 989";}} @media (min-device-height:990px){#S:after{content:" x 990";}} @media (min-device-height:991px){#S:after{content:" x 991";}} @media (min-device-height:992px){#S:after{content:" x 992";}} @media (min-device-height:993px){#S:after{content:" x 993";}} @media (min-device-height:994px){#S:after{content:" x 994";}} @media (min-device-height:995px){#S:after{content:" x 995";}} @media (min-device-height:996px){#S:after{content:" x 996";}} @media (min-device-height:997px){#S:after{content:" x 997";}} @media (min-device-height:998px){#S:after{content:" x 998";}} @media (min-device-height:999px){#S:after{content:" x 999";}} @media (min-device-height:1000px){#S:after{content:" x 1000";}} @media (min-device-height:1001px){#S:after{content:" x 1001";}} @media (min-device-height:1002px){#S:after{content:" x 1002";}} @media (min-device-height:1003px){#S:after{content:" x 1003";}} @media (min-device-height:1004px){#S:after{content:" x 1004";}} @media (min-device-height:1005px){#S:after{content:" x 1005";}} @media (min-device-height:1006px){#S:after{content:" x 1006";}} @media (min-device-height:1007px){#S:after{content:" x 1007";}} @media (min-device-height:1008px){#S:after{content:" x 1008";}} @media (min-device-height:1009px){#S:after{content:" x 1009";}} @media (min-device-height:1010px){#S:after{content:" x 1010";}} @media (min-device-height:1011px){#S:after{content:" x 1011";}} @media (min-device-height:1012px){#S:after{content:" x 1012";}} @media (min-device-height:1013px){#S:after{content:" x 1013";}} @media (min-device-height:1014px){#S:after{content:" x 1014";}} @media (min-device-height:1015px){#S:after{content:" x 1015";}} @media (min-device-height:1016px){#S:after{content:" x 1016";}} @media (min-device-height:1017px){#S:after{content:" x 1017";}} @media (min-device-height:1018px){#S:after{content:" x 1018";}} @media (min-device-height:1019px){#S:after{content:" x 1019";}} @media (min-device-height:1020px){#S:after{content:" x 1020";}} @media (min-device-height:1021px){#S:after{content:" x 1021";}} @media (min-device-height:1022px){#S:after{content:" x 1022";}} @media (min-device-height:1023px){#S:after{content:" x 1023";}} @media (min-device-height:1024px){#S:after{content:" x 1024";}} @media (min-device-height:1025px){#S:after{content:" x 1025";}} @media (min-device-height:1026px){#S:after{content:" x 1026";}} @media (min-device-height:1027px){#S:after{content:" x 1027";}} @media (min-device-height:1028px){#S:after{content:" x 1028";}} @media (min-device-height:1029px){#S:after{content:" x 1029";}} @media (min-device-height:1030px){#S:after{content:" x 1030";}} @media (min-device-height:1031px){#S:after{content:" x 1031";}} @media (min-device-height:1032px){#S:after{content:" x 1032";}} @media (min-device-height:1033px){#S:after{content:" x 1033";}} @media (min-device-height:1034px){#S:after{content:" x 1034";}} @media (min-device-height:1035px){#S:after{content:" x 1035";}} @media (min-device-height:1036px){#S:after{content:" x 1036";}} @media (min-device-height:1037px){#S:after{content:" x 1037";}} @media (min-device-height:1038px){#S:after{content:" x 1038";}} @media (min-device-height:1039px){#S:after{content:" x 1039";}} @media (min-device-height:1040px){#S:after{content:" x 1040";}} @media (min-device-height:1041px){#S:after{content:" x 1041";}} @media (min-device-height:1042px){#S:after{content:" x 1042";}} @media (min-device-height:1043px){#S:after{content:" x 1043";}} @media (min-device-height:1044px){#S:after{content:" x 1044";}} @media (min-device-height:1045px){#S:after{content:" x 1045";}} @media (min-device-height:1046px){#S:after{content:" x 1046";}} @media (min-device-height:1047px){#S:after{content:" x 1047";}} @media (min-device-height:1048px){#S:after{content:" x 1048";}} @media (min-device-height:1049px){#S:after{content:" x 1049";}} @media (min-device-height:1050px){#S:after{content:" x 1050";}} @media (min-device-height:1051px){#S:after{content:" x 1051";}} @media (min-device-height:1052px){#S:after{content:" x 1052";}} @media (min-device-height:1053px){#S:after{content:" x 1053";}} @media (min-device-height:1054px){#S:after{content:" x 1054";}} @media (min-device-height:1055px){#S:after{content:" x 1055";}} @media (min-device-height:1056px){#S:after{content:" x 1056";}} @media (min-device-height:1057px){#S:after{content:" x 1057";}} @media (min-device-height:1058px){#S:after{content:" x 1058";}} @media (min-device-height:1059px){#S:after{content:" x 1059";}} @media (min-device-height:1060px){#S:after{content:" x 1060";}} @media (min-device-height:1061px){#S:after{content:" x 1061";}} @media (min-device-height:1062px){#S:after{content:" x 1062";}} @media (min-device-height:1063px){#S:after{content:" x 1063";}} @media (min-device-height:1064px){#S:after{content:" x 1064";}} @media (min-device-height:1065px){#S:after{content:" x 1065";}} @media (min-device-height:1066px){#S:after{content:" x 1066";}} @media (min-device-height:1067px){#S:after{content:" x 1067";}} @media (min-device-height:1068px){#S:after{content:" x 1068";}} @media (min-device-height:1069px){#S:after{content:" x 1069";}} @media (min-device-height:1070px){#S:after{content:" x 1070";}} @media (min-device-height:1071px){#S:after{content:" x 1071";}} @media (min-device-height:1072px){#S:after{content:" x 1072";}} @media (min-device-height:1073px){#S:after{content:" x 1073";}} @media (min-device-height:1074px){#S:after{content:" x 1074";}} @media (min-device-height:1075px){#S:after{content:" x 1075";}} @media (min-device-height:1076px){#S:after{content:" x 1076";}} @media (min-device-height:1077px){#S:after{content:" x 1077";}} @media (min-device-height:1078px){#S:after{content:" x 1078";}} @media (min-device-height:1079px){#S:after{content:" x 1079";}} @media (min-device-height:1080px){#S:after{content:" x 1080";}} @media (min-device-height:1081px){#S:after{content:" x 1081";}} @media (min-device-height:1082px){#S:after{content:" x 1082";}} @media (min-device-height:1083px){#S:after{content:" x 1083";}} @media (min-device-height:1084px){#S:after{content:" x 1084";}} @media (min-device-height:1085px){#S:after{content:" x 1085";}} @media (min-device-height:1086px){#S:after{content:" x 1086";}} @media (min-device-height:1087px){#S:after{content:" x 1087";}} @media (min-device-height:1088px){#S:after{content:" x 1088";}} @media (min-device-height:1089px){#S:after{content:" x 1089";}} @media (min-device-height:1090px){#S:after{content:" x 1090";}} @media (min-device-height:1091px){#S:after{content:" x 1091";}} @media (min-device-height:1092px){#S:after{content:" x 1092";}} @media (min-device-height:1093px){#S:after{content:" x 1093";}} @media (min-device-height:1094px){#S:after{content:" x 1094";}} @media (min-device-height:1095px){#S:after{content:" x 1095";}} @media (min-device-height:1096px){#S:after{content:" x 1096";}} @media (min-device-height:1097px){#S:after{content:" x 1097";}} @media (min-device-height:1098px){#S:after{content:" x 1098";}} @media (min-device-height:1099px){#S:after{content:" x 1099";}} @media (min-device-height:1100px){#S:after{content:" x 1100";}} @media (min-device-height:1101px){#S:after{content:" x 1101";}} @media (min-device-height:1102px){#S:after{content:" x 1102";}} @media (min-device-height:1103px){#S:after{content:" x 1103";}} @media (min-device-height:1104px){#S:after{content:" x 1104";}} @media (min-device-height:1105px){#S:after{content:" x 1105";}} @media (min-device-height:1106px){#S:after{content:" x 1106";}} @media (min-device-height:1107px){#S:after{content:" x 1107";}} @media (min-device-height:1108px){#S:after{content:" x 1108";}} @media (min-device-height:1109px){#S:after{content:" x 1109";}} @media (min-device-height:1110px){#S:after{content:" x 1110";}} @media (min-device-height:1111px){#S:after{content:" x 1111";}} @media (min-device-height:1112px){#S:after{content:" x 1112";}} @media (min-device-height:1113px){#S:after{content:" x 1113";}} @media (min-device-height:1114px){#S:after{content:" x 1114";}} @media (min-device-height:1115px){#S:after{content:" x 1115";}} @media (min-device-height:1116px){#S:after{content:" x 1116";}} @media (min-device-height:1117px){#S:after{content:" x 1117";}} @media (min-device-height:1118px){#S:after{content:" x 1118";}} @media (min-device-height:1119px){#S:after{content:" x 1119";}} @media (min-device-height:1120px){#S:after{content:" x 1120";}} @media (min-device-height:1121px){#S:after{content:" x 1121";}} @media (min-device-height:1122px){#S:after{content:" x 1122";}} @media (min-device-height:1123px){#S:after{content:" x 1123";}} @media (min-device-height:1124px){#S:after{content:" x 1124";}} @media (min-device-height:1125px){#S:after{content:" x 1125";}} @media (min-device-height:1126px){#S:after{content:" x 1126";}} @media (min-device-height:1127px){#S:after{content:" x 1127";}} @media (min-device-height:1128px){#S:after{content:" x 1128";}} @media (min-device-height:1129px){#S:after{content:" x 1129";}} @media (min-device-height:1130px){#S:after{content:" x 1130";}} @media (min-device-height:1131px){#S:after{content:" x 1131";}} @media (min-device-height:1132px){#S:after{content:" x 1132";}} @media (min-device-height:1133px){#S:after{content:" x 1133";}} @media (min-device-height:1134px){#S:after{content:" x 1134";}} @media (min-device-height:1135px){#S:after{content:" x 1135";}} @media (min-device-height:1136px){#S:after{content:" x 1136";}} @media (min-device-height:1137px){#S:after{content:" x 1137";}} @media (min-device-height:1138px){#S:after{content:" x 1138";}} @media (min-device-height:1139px){#S:after{content:" x 1139";}} @media (min-device-height:1140px){#S:after{content:" x 1140";}} @media (min-device-height:1141px){#S:after{content:" x 1141";}} @media (min-device-height:1142px){#S:after{content:" x 1142";}} @media (min-device-height:1143px){#S:after{content:" x 1143";}} @media (min-device-height:1144px){#S:after{content:" x 1144";}} @media (min-device-height:1145px){#S:after{content:" x 1145";}} @media (min-device-height:1146px){#S:after{content:" x 1146";}} @media (min-device-height:1147px){#S:after{content:" x 1147";}} @media (min-device-height:1148px){#S:after{content:" x 1148";}} @media (min-device-height:1149px){#S:after{content:" x 1149";}} @media (min-device-height:1150px){#S:after{content:" x 1150";}} @media (min-device-height:1151px){#S:after{content:" x 1151";}} @media (min-device-height:1152px){#S:after{content:" x 1152";}} @media (min-device-height:1153px){#S:after{content:" x 1153";}} @media (min-device-height:1154px){#S:after{content:" x 1154";}} @media (min-device-height:1155px){#S:after{content:" x 1155";}} @media (min-device-height:1156px){#S:after{content:" x 1156";}} @media (min-device-height:1157px){#S:after{content:" x 1157";}} @media (min-device-height:1158px){#S:after{content:" x 1158";}} @media (min-device-height:1159px){#S:after{content:" x 1159";}} @media (min-device-height:1160px){#S:after{content:" x 1160";}} @media (min-device-height:1161px){#S:after{content:" x 1161";}} @media (min-device-height:1162px){#S:after{content:" x 1162";}} @media (min-device-height:1163px){#S:after{content:" x 1163";}} @media (min-device-height:1164px){#S:after{content:" x 1164";}} @media (min-device-height:1165px){#S:after{content:" x 1165";}} @media (min-device-height:1166px){#S:after{content:" x 1166";}} @media (min-device-height:1167px){#S:after{content:" x 1167";}} @media (min-device-height:1168px){#S:after{content:" x 1168";}} @media (min-device-height:1169px){#S:after{content:" x 1169";}} @media (min-device-height:1170px){#S:after{content:" x 1170";}} @media (min-device-height:1171px){#S:after{content:" x 1171";}} @media (min-device-height:1172px){#S:after{content:" x 1172";}} @media (min-device-height:1173px){#S:after{content:" x 1173";}} @media (min-device-height:1174px){#S:after{content:" x 1174";}} @media (min-device-height:1175px){#S:after{content:" x 1175";}} @media (min-device-height:1176px){#S:after{content:" x 1176";}} @media (min-device-height:1177px){#S:after{content:" x 1177";}} @media (min-device-height:1178px){#S:after{content:" x 1178";}} @media (min-device-height:1179px){#S:after{content:" x 1179";}} @media (min-device-height:1180px){#S:after{content:" x 1180";}} @media (min-device-height:1181px){#S:after{content:" x 1181";}} @media (min-device-height:1182px){#S:after{content:" x 1182";}} @media (min-device-height:1183px){#S:after{content:" x 1183";}} @media (min-device-height:1184px){#S:after{content:" x 1184";}} @media (min-device-height:1185px){#S:after{content:" x 1185";}} @media (min-device-height:1186px){#S:after{content:" x 1186";}} @media (min-device-height:1187px){#S:after{content:" x 1187";}} @media (min-device-height:1188px){#S:after{content:" x 1188";}} @media (min-device-height:1189px){#S:after{content:" x 1189";}} @media (min-device-height:1190px){#S:after{content:" x 1190";}} @media (min-device-height:1191px){#S:after{content:" x 1191";}} @media (min-device-height:1192px){#S:after{content:" x 1192";}} @media (min-device-height:1193px){#S:after{content:" x 1193";}} @media (min-device-height:1194px){#S:after{content:" x 1194";}} @media (min-device-height:1195px){#S:after{content:" x 1195";}} @media (min-device-height:1196px){#S:after{content:" x 1196";}} @media (min-device-height:1197px){#S:after{content:" x 1197";}} @media (min-device-height:1198px){#S:after{content:" x 1198";}} @media (min-device-height:1199px){#S:after{content:" x 1199";}} @media (min-device-height:1200px){#S:after{content:" x 1200";}} @media (min-device-height:1201px){#S:after{content:" x 1201";}} @media (min-device-height:1202px){#S:after{content:" x 1202";}} @media (min-device-height:1203px){#S:after{content:" x 1203";}} @media (min-device-height:1204px){#S:after{content:" x 1204";}} @media (min-device-height:1205px){#S:after{content:" x 1205";}} @media (min-device-height:1206px){#S:after{content:" x 1206";}} @media (min-device-height:1207px){#S:after{content:" x 1207";}} @media (min-device-height:1208px){#S:after{content:" x 1208";}} @media (min-device-height:1209px){#S:after{content:" x 1209";}} @media (min-device-height:1210px){#S:after{content:" x 1210";}} @media (min-device-height:1211px){#S:after{content:" x 1211";}} @media (min-device-height:1212px){#S:after{content:" x 1212";}} @media (min-device-height:1213px){#S:after{content:" x 1213";}} @media (min-device-height:1214px){#S:after{content:" x 1214";}} @media (min-device-height:1215px){#S:after{content:" x 1215";}} @media (min-device-height:1216px){#S:after{content:" x 1216";}} @media (min-device-height:1217px){#S:after{content:" x 1217";}} @media (min-device-height:1218px){#S:after{content:" x 1218";}} @media (min-device-height:1219px){#S:after{content:" x 1219";}} @media (min-device-height:1220px){#S:after{content:" x 1220";}} @media (min-device-height:1221px){#S:after{content:" x 1221";}} @media (min-device-height:1222px){#S:after{content:" x 1222";}} @media (min-device-height:1223px){#S:after{content:" x 1223";}} @media (min-device-height:1224px){#S:after{content:" x 1224";}} @media (min-device-height:1225px){#S:after{content:" x 1225";}} @media (min-device-height:1226px){#S:after{content:" x 1226";}} @media (min-device-height:1227px){#S:after{content:" x 1227";}} @media (min-device-height:1228px){#S:after{content:" x 1228";}} @media (min-device-height:1229px){#S:after{content:" x 1229";}} @media (min-device-height:1230px){#S:after{content:" x 1230";}} @media (min-device-height:1231px){#S:after{content:" x 1231";}} @media (min-device-height:1232px){#S:after{content:" x 1232";}} @media (min-device-height:1233px){#S:after{content:" x 1233";}} @media (min-device-height:1234px){#S:after{content:" x 1234";}} @media (min-device-height:1235px){#S:after{content:" x 1235";}} @media (min-device-height:1236px){#S:after{content:" x 1236";}} @media (min-device-height:1237px){#S:after{content:" x 1237";}} @media (min-device-height:1238px){#S:after{content:" x 1238";}} @media (min-device-height:1239px){#S:after{content:" x 1239";}} @media (min-device-height:1240px){#S:after{content:" x 1240";}} @media (min-device-height:1241px){#S:after{content:" x 1241";}} @media (min-device-height:1242px){#S:after{content:" x 1242";}} @media (min-device-height:1243px){#S:after{content:" x 1243";}} @media (min-device-height:1244px){#S:after{content:" x 1244";}} @media (min-device-height:1245px){#S:after{content:" x 1245";}} @media (min-device-height:1246px){#S:after{content:" x 1246";}} @media (min-device-height:1247px){#S:after{content:" x 1247";}} @media (min-device-height:1248px){#S:after{content:" x 1248";}} @media (min-device-height:1249px){#S:after{content:" x 1249";}} @media (min-device-height:1250px){#S:after{content:" x 1250";}} @media (min-device-height:1251px){#S:after{content:" x 1251";}} @media (min-device-height:1252px){#S:after{content:" x 1252";}} @media (min-device-height:1253px){#S:after{content:" x 1253";}} @media (min-device-height:1254px){#S:after{content:" x 1254";}} @media (min-device-height:1255px){#S:after{content:" x 1255";}} @media (min-device-height:1256px){#S:after{content:" x 1256";}} @media (min-device-height:1257px){#S:after{content:" x 1257";}} @media (min-device-height:1258px){#S:after{content:" x 1258";}} @media (min-device-height:1259px){#S:after{content:" x 1259";}} @media (min-device-height:1260px){#S:after{content:" x 1260";}} @media (min-device-height:1261px){#S:after{content:" x 1261";}} @media (min-device-height:1262px){#S:after{content:" x 1262";}} @media (min-device-height:1263px){#S:after{content:" x 1263";}} @media (min-device-height:1264px){#S:after{content:" x 1264";}} @media (min-device-height:1265px){#S:after{content:" x 1265";}} @media (min-device-height:1266px){#S:after{content:" x 1266";}} @media (min-device-height:1267px){#S:after{content:" x 1267";}} @media (min-device-height:1268px){#S:after{content:" x 1268";}} @media (min-device-height:1269px){#S:after{content:" x 1269";}} @media (min-device-height:1270px){#S:after{content:" x 1270";}} @media (min-device-height:1271px){#S:after{content:" x 1271";}} @media (min-device-height:1272px){#S:after{content:" x 1272";}} @media (min-device-height:1273px){#S:after{content:" x 1273";}} @media (min-device-height:1274px){#S:after{content:" x 1274";}} @media (min-device-height:1275px){#S:after{content:" x 1275";}} @media (min-device-height:1276px){#S:after{content:" x 1276";}} @media (min-device-height:1277px){#S:after{content:" x 1277";}} @media (min-device-height:1278px){#S:after{content:" x 1278";}} @media (min-device-height:1279px){#S:after{content:" x 1279";}} @media (min-device-height:1280px){#S:after{content:" x 1280";}} @media (min-device-height:1281px){#S:after{content:" x 1281";}} @media (min-device-height:1282px){#S:after{content:" x 1282";}} @media (min-device-height:1283px){#S:after{content:" x 1283";}} @media (min-device-height:1284px){#S:after{content:" x 1284";}} @media (min-device-height:1285px){#S:after{content:" x 1285";}} @media (min-device-height:1286px){#S:after{content:" x 1286";}} @media (min-device-height:1287px){#S:after{content:" x 1287";}} @media (min-device-height:1288px){#S:after{content:" x 1288";}} @media (min-device-height:1289px){#S:after{content:" x 1289";}} @media (min-device-height:1290px){#S:after{content:" x 1290";}} @media (min-device-height:1291px){#S:after{content:" x 1291";}} @media (min-device-height:1292px){#S:after{content:" x 1292";}} @media (min-device-height:1293px){#S:after{content:" x 1293";}} @media (min-device-height:1294px){#S:after{content:" x 1294";}} @media (min-device-height:1295px){#S:after{content:" x 1295";}} @media (min-device-height:1296px){#S:after{content:" x 1296";}} @media (min-device-height:1297px){#S:after{content:" x 1297";}} @media (min-device-height:1298px){#S:after{content:" x 1298";}} @media (min-device-height:1299px){#S:after{content:" x 1299";}} @media (min-device-height:1300px){#S:after{content:" x 1300";}} @media (min-device-height:1301px){#S:after{content:" x 1301";}} @media (min-device-height:1302px){#S:after{content:" x 1302";}} @media (min-device-height:1303px){#S:after{content:" x 1303";}} @media (min-device-height:1304px){#S:after{content:" x 1304";}} @media (min-device-height:1305px){#S:after{content:" x 1305";}} @media (min-device-height:1306px){#S:after{content:" x 1306";}} @media (min-device-height:1307px){#S:after{content:" x 1307";}} @media (min-device-height:1308px){#S:after{content:" x 1308";}} @media (min-device-height:1309px){#S:after{content:" x 1309";}} @media (min-device-height:1310px){#S:after{content:" x 1310";}} @media (min-device-height:1311px){#S:after{content:" x 1311";}} @media (min-device-height:1312px){#S:after{content:" x 1312";}} @media (min-device-height:1313px){#S:after{content:" x 1313";}} @media (min-device-height:1314px){#S:after{content:" x 1314";}} @media (min-device-height:1315px){#S:after{content:" x 1315";}} @media (min-device-height:1316px){#S:after{content:" x 1316";}} @media (min-device-height:1317px){#S:after{content:" x 1317";}} @media (min-device-height:1318px){#S:after{content:" x 1318";}} @media (min-device-height:1319px){#S:after{content:" x 1319";}} @media (min-device-height:1320px){#S:after{content:" x 1320";}} @media (min-device-height:1321px){#S:after{content:" x 1321";}} @media (min-device-height:1322px){#S:after{content:" x 1322";}} @media (min-device-height:1323px){#S:after{content:" x 1323";}} @media (min-device-height:1324px){#S:after{content:" x 1324";}} @media (min-device-height:1325px){#S:after{content:" x 1325";}} @media (min-device-height:1326px){#S:after{content:" x 1326";}} @media (min-device-height:1327px){#S:after{content:" x 1327";}} @media (min-device-height:1328px){#S:after{content:" x 1328";}} @media (min-device-height:1329px){#S:after{content:" x 1329";}} @media (min-device-height:1330px){#S:after{content:" x 1330";}} @media (min-device-height:1331px){#S:after{content:" x 1331";}} @media (min-device-height:1332px){#S:after{content:" x 1332";}} @media (min-device-height:1333px){#S:after{content:" x 1333";}} @media (min-device-height:1334px){#S:after{content:" x 1334";}} @media (min-device-height:1335px){#S:after{content:" x 1335";}} @media (min-device-height:1336px){#S:after{content:" x 1336";}} @media (min-device-height:1337px){#S:after{content:" x 1337";}} @media (min-device-height:1338px){#S:after{content:" x 1338";}} @media (min-device-height:1339px){#S:after{content:" x 1339";}} @media (min-device-height:1340px){#S:after{content:" x 1340";}} @media (min-device-height:1341px){#S:after{content:" x 1341";}} @media (min-device-height:1342px){#S:after{content:" x 1342";}} @media (min-device-height:1343px){#S:after{content:" x 1343";}} @media (min-device-height:1344px){#S:after{content:" x 1344";}} @media (min-device-height:1345px){#S:after{content:" x 1345";}} @media (min-device-height:1346px){#S:after{content:" x 1346";}} @media (min-device-height:1347px){#S:after{content:" x 1347";}} @media (min-device-height:1348px){#S:after{content:" x 1348";}} @media (min-device-height:1349px){#S:after{content:" x 1349";}} @media (min-device-height:1350px){#S:after{content:" x 1350";}} @media (min-device-height:1351px){#S:after{content:" x 1351";}} @media (min-device-height:1352px){#S:after{content:" x 1352";}} @media (min-device-height:1353px){#S:after{content:" x 1353";}} @media (min-device-height:1354px){#S:after{content:" x 1354";}} @media (min-device-height:1355px){#S:after{content:" x 1355";}} @media (min-device-height:1356px){#S:after{content:" x 1356";}} @media (min-device-height:1357px){#S:after{content:" x 1357";}} @media (min-device-height:1358px){#S:after{content:" x 1358";}} @media (min-device-height:1359px){#S:after{content:" x 1359";}} @media (min-device-height:1360px){#S:after{content:" x 1360";}} @media (min-device-height:1361px){#S:after{content:" x 1361";}} @media (min-device-height:1362px){#S:after{content:" x 1362";}} @media (min-device-height:1363px){#S:after{content:" x 1363";}} @media (min-device-height:1364px){#S:after{content:" x 1364";}} @media (min-device-height:1365px){#S:after{content:" x 1365";}} @media (min-device-height:1366px){#S:after{content:" x 1366";}} @media (min-device-height:1367px){#S:after{content:" x 1367";}} @media (min-device-height:1368px){#S:after{content:" x 1368";}} @media (min-device-height:1369px){#S:after{content:" x 1369";}} @media (min-device-height:1370px){#S:after{content:" x 1370";}} @media (min-device-height:1371px){#S:after{content:" x 1371";}} @media (min-device-height:1372px){#S:after{content:" x 1372";}} @media (min-device-height:1373px){#S:after{content:" x 1373";}} @media (min-device-height:1374px){#S:after{content:" x 1374";}} @media (min-device-height:1375px){#S:after{content:" x 1375";}} @media (min-device-height:1376px){#S:after{content:" x 1376";}} @media (min-device-height:1377px){#S:after{content:" x 1377";}} @media (min-device-height:1378px){#S:after{content:" x 1378";}} @media (min-device-height:1379px){#S:after{content:" x 1379";}} @media (min-device-height:1380px){#S:after{content:" x 1380";}} @media (min-device-height:1381px){#S:after{content:" x 1381";}} @media (min-device-height:1382px){#S:after{content:" x 1382";}} @media (min-device-height:1383px){#S:after{content:" x 1383";}} @media (min-device-height:1384px){#S:after{content:" x 1384";}} @media (min-device-height:1385px){#S:after{content:" x 1385";}} @media (min-device-height:1386px){#S:after{content:" x 1386";}} @media (min-device-height:1387px){#S:after{content:" x 1387";}} @media (min-device-height:1388px){#S:after{content:" x 1388";}} @media (min-device-height:1389px){#S:after{content:" x 1389";}} @media (min-device-height:1390px){#S:after{content:" x 1390";}} @media (min-device-height:1391px){#S:after{content:" x 1391";}} @media (min-device-height:1392px){#S:after{content:" x 1392";}} @media (min-device-height:1393px){#S:after{content:" x 1393";}} @media (min-device-height:1394px){#S:after{content:" x 1394";}} @media (min-device-height:1395px){#S:after{content:" x 1395";}} @media (min-device-height:1396px){#S:after{content:" x 1396";}} @media (min-device-height:1397px){#S:after{content:" x 1397";}} @media (min-device-height:1398px){#S:after{content:" x 1398";}} @media (min-device-height:1399px){#S:after{content:" x 1399";}} @media (min-device-height:1400px){#S:after{content:" x 1400";}} @media (min-device-height:1401px){#S:after{content:" x 1401";}} @media (min-device-height:1402px){#S:after{content:" x 1402";}} @media (min-device-height:1403px){#S:after{content:" x 1403";}} @media (min-device-height:1404px){#S:after{content:" x 1404";}} @media (min-device-height:1405px){#S:after{content:" x 1405";}} @media (min-device-height:1406px){#S:after{content:" x 1406";}} @media (min-device-height:1407px){#S:after{content:" x 1407";}} @media (min-device-height:1408px){#S:after{content:" x 1408";}} @media (min-device-height:1409px){#S:after{content:" x 1409";}} @media (min-device-height:1410px){#S:after{content:" x 1410";}} @media (min-device-height:1411px){#S:after{content:" x 1411";}} @media (min-device-height:1412px){#S:after{content:" x 1412";}} @media (min-device-height:1413px){#S:after{content:" x 1413";}} @media (min-device-height:1414px){#S:after{content:" x 1414";}} @media (min-device-height:1415px){#S:after{content:" x 1415";}} @media (min-device-height:1416px){#S:after{content:" x 1416";}} @media (min-device-height:1417px){#S:after{content:" x 1417";}} @media (min-device-height:1418px){#S:after{content:" x 1418";}} @media (min-device-height:1419px){#S:after{content:" x 1419";}} @media (min-device-height:1420px){#S:after{content:" x 1420";}} @media (min-device-height:1421px){#S:after{content:" x 1421";}} @media (min-device-height:1422px){#S:after{content:" x 1422";}} @media (min-device-height:1423px){#S:after{content:" x 1423";}} @media (min-device-height:1424px){#S:after{content:" x 1424";}} @media (min-device-height:1425px){#S:after{content:" x 1425";}} @media (min-device-height:1426px){#S:after{content:" x 1426";}} @media (min-device-height:1427px){#S:after{content:" x 1427";}} @media (min-device-height:1428px){#S:after{content:" x 1428";}} @media (min-device-height:1429px){#S:after{content:" x 1429";}} @media (min-device-height:1430px){#S:after{content:" x 1430";}} @media (min-device-height:1431px){#S:after{content:" x 1431";}} @media (min-device-height:1432px){#S:after{content:" x 1432";}} @media (min-device-height:1433px){#S:after{content:" x 1433";}} @media (min-device-height:1434px){#S:after{content:" x 1434";}} @media (min-device-height:1435px){#S:after{content:" x 1435";}} @media (min-device-height:1436px){#S:after{content:" x 1436";}} @media (min-device-height:1437px){#S:after{content:" x 1437";}} @media (min-device-height:1438px){#S:after{content:" x 1438";}} @media (min-device-height:1439px){#S:after{content:" x 1439";}} @media (min-device-height:1440px){#S:after{content:" x 1440";}} @media (min-device-height:1441px){#S:after{content:" x 1441";}} @media (min-device-height:1442px){#S:after{content:" x 1442";}} @media (min-device-height:1443px){#S:after{content:" x 1443";}} @media (min-device-height:1444px){#S:after{content:" x 1444";}} @media (min-device-height:1445px){#S:after{content:" x 1445";}} @media (min-device-height:1446px){#S:after{content:" x 1446";}} @media (min-device-height:1447px){#S:after{content:" x 1447";}} @media (min-device-height:1448px){#S:after{content:" x 1448";}} @media (min-device-height:1449px){#S:after{content:" x 1449";}} @media (min-device-height:1450px){#S:after{content:" x 1450";}} @media (min-device-height:1451px){#S:after{content:" x 1451";}} @media (min-device-height:1452px){#S:after{content:" x 1452";}} @media (min-device-height:1453px){#S:after{content:" x 1453";}} @media (min-device-height:1454px){#S:after{content:" x 1454";}} @media (min-device-height:1455px){#S:after{content:" x 1455";}} @media (min-device-height:1456px){#S:after{content:" x 1456";}} @media (min-device-height:1457px){#S:after{content:" x 1457";}} @media (min-device-height:1458px){#S:after{content:" x 1458";}} @media (min-device-height:1459px){#S:after{content:" x 1459";}} @media (min-device-height:1460px){#S:after{content:" x 1460";}} @media (min-device-height:1461px){#S:after{content:" x 1461";}} @media (min-device-height:1462px){#S:after{content:" x 1462";}} @media (min-device-height:1463px){#S:after{content:" x 1463";}} @media (min-device-height:1464px){#S:after{content:" x 1464";}} @media (min-device-height:1465px){#S:after{content:" x 1465";}} @media (min-device-height:1466px){#S:after{content:" x 1466";}} @media (min-device-height:1467px){#S:after{content:" x 1467";}} @media (min-device-height:1468px){#S:after{content:" x 1468";}} @media (min-device-height:1469px){#S:after{content:" x 1469";}} @media (min-device-height:1470px){#S:after{content:" x 1470";}} @media (min-device-height:1471px){#S:after{content:" x 1471";}} @media (min-device-height:1472px){#S:after{content:" x 1472";}} @media (min-device-height:1473px){#S:after{content:" x 1473";}} @media (min-device-height:1474px){#S:after{content:" x 1474";}} @media (min-device-height:1475px){#S:after{content:" x 1475";}} @media (min-device-height:1476px){#S:after{content:" x 1476";}} @media (min-device-height:1477px){#S:after{content:" x 1477";}} @media (min-device-height:1478px){#S:after{content:" x 1478";}} @media (min-device-height:1479px){#S:after{content:" x 1479";}} @media (min-device-height:1480px){#S:after{content:" x 1480";}} @media (min-device-height:1481px){#S:after{content:" x 1481";}} @media (min-device-height:1482px){#S:after{content:" x 1482";}} @media (min-device-height:1483px){#S:after{content:" x 1483";}} @media (min-device-height:1484px){#S:after{content:" x 1484";}} @media (min-device-height:1485px){#S:after{content:" x 1485";}} @media (min-device-height:1486px){#S:after{content:" x 1486";}} @media (min-device-height:1487px){#S:after{content:" x 1487";}} @media (min-device-height:1488px){#S:after{content:" x 1488";}} @media (min-device-height:1489px){#S:after{content:" x 1489";}} @media (min-device-height:1490px){#S:after{content:" x 1490";}} @media (min-device-height:1491px){#S:after{content:" x 1491";}} @media (min-device-height:1492px){#S:after{content:" x 1492";}} @media (min-device-height:1493px){#S:after{content:" x 1493";}} @media (min-device-height:1494px){#S:after{content:" x 1494";}} @media (min-device-height:1495px){#S:after{content:" x 1495";}} @media (min-device-height:1496px){#S:after{content:" x 1496";}} @media (min-device-height:1497px){#S:after{content:" x 1497";}} @media (min-device-height:1498px){#S:after{content:" x 1498";}} @media (min-device-height:1499px){#S:after{content:" x 1499";}} @media (min-device-height:1500px){#S:after{content:" x 1500";}} @media (min-device-height:1501px){#S:after{content:" x 1501";}} @media (min-device-height:1502px){#S:after{content:" x 1502";}} @media (min-device-height:1503px){#S:after{content:" x 1503";}} @media (min-device-height:1504px){#S:after{content:" x 1504";}} @media (min-device-height:1505px){#S:after{content:" x 1505";}} @media (min-device-height:1506px){#S:after{content:" x 1506";}} @media (min-device-height:1507px){#S:after{content:" x 1507";}} @media (min-device-height:1508px){#S:after{content:" x 1508";}} @media (min-device-height:1509px){#S:after{content:" x 1509";}} @media (min-device-height:1510px){#S:after{content:" x 1510";}} @media (min-device-height:1511px){#S:after{content:" x 1511";}} @media (min-device-height:1512px){#S:after{content:" x 1512";}} @media (min-device-height:1513px){#S:after{content:" x 1513";}} @media (min-device-height:1514px){#S:after{content:" x 1514";}} @media (min-device-height:1515px){#S:after{content:" x 1515";}} @media (min-device-height:1516px){#S:after{content:" x 1516";}} @media (min-device-height:1517px){#S:after{content:" x 1517";}} @media (min-device-height:1518px){#S:after{content:" x 1518";}} @media (min-device-height:1519px){#S:after{content:" x 1519";}} @media (min-device-height:1520px){#S:after{content:" x 1520";}} @media (min-device-height:1521px){#S:after{content:" x 1521";}} @media (min-device-height:1522px){#S:after{content:" x 1522";}} @media (min-device-height:1523px){#S:after{content:" x 1523";}} @media (min-device-height:1524px){#S:after{content:" x 1524";}} @media (min-device-height:1525px){#S:after{content:" x 1525";}} @media (min-device-height:1526px){#S:after{content:" x 1526";}} @media (min-device-height:1527px){#S:after{content:" x 1527";}} @media (min-device-height:1528px){#S:after{content:" x 1528";}} @media (min-device-height:1529px){#S:after{content:" x 1529";}} @media (min-device-height:1530px){#S:after{content:" x 1530";}} @media (min-device-height:1531px){#S:after{content:" x 1531";}} @media (min-device-height:1532px){#S:after{content:" x 1532";}} @media (min-device-height:1533px){#S:after{content:" x 1533";}} @media (min-device-height:1534px){#S:after{content:" x 1534";}} @media (min-device-height:1535px){#S:after{content:" x 1535";}} @media (min-device-height:1536px){#S:after{content:" x 1536";}} @media (min-device-height:1537px){#S:after{content:" x 1537";}} @media (min-device-height:1538px){#S:after{content:" x 1538";}} @media (min-device-height:1539px){#S:after{content:" x 1539";}} @media (min-device-height:1540px){#S:after{content:" x 1540";}} @media (min-device-height:1541px){#S:after{content:" x 1541";}} @media (min-device-height:1542px){#S:after{content:" x 1542";}} @media (min-device-height:1543px){#S:after{content:" x 1543";}} @media (min-device-height:1544px){#S:after{content:" x 1544";}} @media (min-device-height:1545px){#S:after{content:" x 1545";}} @media (min-device-height:1546px){#S:after{content:" x 1546";}} @media (min-device-height:1547px){#S:after{content:" x 1547";}} @media (min-device-height:1548px){#S:after{content:" x 1548";}} @media (min-device-height:1549px){#S:after{content:" x 1549";}} @media (min-device-height:1550px){#S:after{content:" x 1550";}} @media (min-device-height:1551px){#S:after{content:" x 1551";}} @media (min-device-height:1552px){#S:after{content:" x 1552";}} @media (min-device-height:1553px){#S:after{content:" x 1553";}} @media (min-device-height:1554px){#S:after{content:" x 1554";}} @media (min-device-height:1555px){#S:after{content:" x 1555";}} @media (min-device-height:1556px){#S:after{content:" x 1556";}} @media (min-device-height:1557px){#S:after{content:" x 1557";}} @media (min-device-height:1558px){#S:after{content:" x 1558";}} @media (min-device-height:1559px){#S:after{content:" x 1559";}} @media (min-device-height:1560px){#S:after{content:" x 1560";}} @media (min-device-height:1561px){#S:after{content:" x 1561";}} @media (min-device-height:1562px){#S:after{content:" x 1562";}} @media (min-device-height:1563px){#S:after{content:" x 1563";}} @media (min-device-height:1564px){#S:after{content:" x 1564";}} @media (min-device-height:1565px){#S:after{content:" x 1565";}} @media (min-device-height:1566px){#S:after{content:" x 1566";}} @media (min-device-height:1567px){#S:after{content:" x 1567";}} @media (min-device-height:1568px){#S:after{content:" x 1568";}} @media (min-device-height:1569px){#S:after{content:" x 1569";}} @media (min-device-height:1570px){#S:after{content:" x 1570";}} @media (min-device-height:1571px){#S:after{content:" x 1571";}} @media (min-device-height:1572px){#S:after{content:" x 1572";}} @media (min-device-height:1573px){#S:after{content:" x 1573";}} @media (min-device-height:1574px){#S:after{content:" x 1574";}} @media (min-device-height:1575px){#S:after{content:" x 1575";}} @media (min-device-height:1576px){#S:after{content:" x 1576";}} @media (min-device-height:1577px){#S:after{content:" x 1577";}} @media (min-device-height:1578px){#S:after{content:" x 1578";}} @media (min-device-height:1579px){#S:after{content:" x 1579";}} @media (min-device-height:1580px){#S:after{content:" x 1580";}} @media (min-device-height:1581px){#S:after{content:" x 1581";}} @media (min-device-height:1582px){#S:after{content:" x 1582";}} @media (min-device-height:1583px){#S:after{content:" x 1583";}} @media (min-device-height:1584px){#S:after{content:" x 1584";}} @media (min-device-height:1585px){#S:after{content:" x 1585";}} @media (min-device-height:1586px){#S:after{content:" x 1586";}} @media (min-device-height:1587px){#S:after{content:" x 1587";}} @media (min-device-height:1588px){#S:after{content:" x 1588";}} @media (min-device-height:1589px){#S:after{content:" x 1589";}} @media (min-device-height:1590px){#S:after{content:" x 1590";}} @media (min-device-height:1591px){#S:after{content:" x 1591";}} @media (min-device-height:1592px){#S:after{content:" x 1592";}} @media (min-device-height:1593px){#S:after{content:" x 1593";}} @media (min-device-height:1594px){#S:after{content:" x 1594";}} @media (min-device-height:1595px){#S:after{content:" x 1595";}} @media (min-device-height:1596px){#S:after{content:" x 1596";}} @media (min-device-height:1597px){#S:after{content:" x 1597";}} @media (min-device-height:1598px){#S:after{content:" x 1598";}} @media (min-device-height:1599px){#S:after{content:" x 1599";}} @media (min-device-height:1600px){#S:after{content:" x 1600";}} @media (min-device-height:1601px){#S:after{content:" x 1601";}} @media (min-device-height:1602px){#S:after{content:" x 1602";}} @media (min-device-height:1603px){#S:after{content:" x 1603";}} @media (min-device-height:1604px){#S:after{content:" x 1604";}} @media (min-device-height:1605px){#S:after{content:" x 1605";}} @media (min-device-height:1606px){#S:after{content:" x 1606";}} @media (min-device-height:1607px){#S:after{content:" x 1607";}} @media (min-device-height:1608px){#S:after{content:" x 1608";}} @media (min-device-height:1609px){#S:after{content:" x 1609";}} @media (min-device-height:1610px){#S:after{content:" x 1610";}} @media (min-device-height:1611px){#S:after{content:" x 1611";}} @media (min-device-height:1612px){#S:after{content:" x 1612";}} @media (min-device-height:1613px){#S:after{content:" x 1613";}} @media (min-device-height:1614px){#S:after{content:" x 1614";}} @media (min-device-height:1615px){#S:after{content:" x 1615";}} @media (min-device-height:1616px){#S:after{content:" x 1616";}} @media (min-device-height:1617px){#S:after{content:" x 1617";}} @media (min-device-height:1618px){#S:after{content:" x 1618";}} @media (min-device-height:1619px){#S:after{content:" x 1619";}} @media (min-device-height:1620px){#S:after{content:" x 1620";}} @media (min-device-height:1621px){#S:after{content:" x 1621";}} @media (min-device-height:1622px){#S:after{content:" x 1622";}} @media (min-device-height:1623px){#S:after{content:" x 1623";}} @media (min-device-height:1624px){#S:after{content:" x 1624";}} @media (min-device-height:1625px){#S:after{content:" x 1625";}} @media (min-device-height:1626px){#S:after{content:" x 1626";}} @media (min-device-height:1627px){#S:after{content:" x 1627";}} @media (min-device-height:1628px){#S:after{content:" x 1628";}} @media (min-device-height:1629px){#S:after{content:" x 1629";}} @media (min-device-height:1630px){#S:after{content:" x 1630";}} @media (min-device-height:1631px){#S:after{content:" x 1631";}} @media (min-device-height:1632px){#S:after{content:" x 1632";}} @media (min-device-height:1633px){#S:after{content:" x 1633";}} @media (min-device-height:1634px){#S:after{content:" x 1634";}} @media (min-device-height:1635px){#S:after{content:" x 1635";}} @media (min-device-height:1636px){#S:after{content:" x 1636";}} @media (min-device-height:1637px){#S:after{content:" x 1637";}} @media (min-device-height:1638px){#S:after{content:" x 1638";}} @media (min-device-height:1639px){#S:after{content:" x 1639";}} @media (min-device-height:1640px){#S:after{content:" x 1640";}} @media (min-device-height:1641px){#S:after{content:" x 1641";}} @media (min-device-height:1642px){#S:after{content:" x 1642";}} @media (min-device-height:1643px){#S:after{content:" x 1643";}} @media (min-device-height:1644px){#S:after{content:" x 1644";}} @media (min-device-height:1645px){#S:after{content:" x 1645";}} @media (min-device-height:1646px){#S:after{content:" x 1646";}} @media (min-device-height:1647px){#S:after{content:" x 1647";}} @media (min-device-height:1648px){#S:after{content:" x 1648";}} @media (min-device-height:1649px){#S:after{content:" x 1649";}} @media (min-device-height:1650px){#S:after{content:" x 1650";}} @media (min-device-height:1651px){#S:after{content:" x 1651";}} @media (min-device-height:1652px){#S:after{content:" x 1652";}} @media (min-device-height:1653px){#S:after{content:" x 1653";}} @media (min-device-height:1654px){#S:after{content:" x 1654";}} @media (min-device-height:1655px){#S:after{content:" x 1655";}} @media (min-device-height:1656px){#S:after{content:" x 1656";}} @media (min-device-height:1657px){#S:after{content:" x 1657";}} @media (min-device-height:1658px){#S:after{content:" x 1658";}} @media (min-device-height:1659px){#S:after{content:" x 1659";}} @media (min-device-height:1660px){#S:after{content:" x 1660";}} @media (min-device-height:1661px){#S:after{content:" x 1661";}} @media (min-device-height:1662px){#S:after{content:" x 1662";}} @media (min-device-height:1663px){#S:after{content:" x 1663";}} @media (min-device-height:1664px){#S:after{content:" x 1664";}} @media (min-device-height:1665px){#S:after{content:" x 1665";}} @media (min-device-height:1666px){#S:after{content:" x 1666";}} @media (min-device-height:1667px){#S:after{content:" x 1667";}} @media (min-device-height:1668px){#S:after{content:" x 1668";}} @media (min-device-height:1669px){#S:after{content:" x 1669";}} @media (min-device-height:1670px){#S:after{content:" x 1670";}} @media (min-device-height:1671px){#S:after{content:" x 1671";}} @media (min-device-height:1672px){#S:after{content:" x 1672";}} @media (min-device-height:1673px){#S:after{content:" x 1673";}} @media (min-device-height:1674px){#S:after{content:" x 1674";}} @media (min-device-height:1675px){#S:after{content:" x 1675";}} @media (min-device-height:1676px){#S:after{content:" x 1676";}} @media (min-device-height:1677px){#S:after{content:" x 1677";}} @media (min-device-height:1678px){#S:after{content:" x 1678";}} @media (min-device-height:1679px){#S:after{content:" x 1679";}} @media (min-device-height:1680px){#S:after{content:" x 1680";}} @media (min-device-height:1681px){#S:after{content:" x 1681";}} @media (min-device-height:1682px){#S:after{content:" x 1682";}} @media (min-device-height:1683px){#S:after{content:" x 1683";}} @media (min-device-height:1684px){#S:after{content:" x 1684";}} @media (min-device-height:1685px){#S:after{content:" x 1685";}} @media (min-device-height:1686px){#S:after{content:" x 1686";}} @media (min-device-height:1687px){#S:after{content:" x 1687";}} @media (min-device-height:1688px){#S:after{content:" x 1688";}} @media (min-device-height:1689px){#S:after{content:" x 1689";}} @media (min-device-height:1690px){#S:after{content:" x 1690";}} @media (min-device-height:1691px){#S:after{content:" x 1691";}} @media (min-device-height:1692px){#S:after{content:" x 1692";}} @media (min-device-height:1693px){#S:after{content:" x 1693";}} @media (min-device-height:1694px){#S:after{content:" x 1694";}} @media (min-device-height:1695px){#S:after{content:" x 1695";}} @media (min-device-height:1696px){#S:after{content:" x 1696";}} @media (min-device-height:1697px){#S:after{content:" x 1697";}} @media (min-device-height:1698px){#S:after{content:" x 1698";}} @media (min-device-height:1699px){#S:after{content:" x 1699";}} @media (min-device-height:1700px){#S:after{content:" x 1700";}} @media (min-device-height:1701px){#S:after{content:" x 1701";}} @media (min-device-height:1702px){#S:after{content:" x 1702";}} @media (min-device-height:1703px){#S:after{content:" x 1703";}} @media (min-device-height:1704px){#S:after{content:" x 1704";}} @media (min-device-height:1705px){#S:after{content:" x 1705";}} @media (min-device-height:1706px){#S:after{content:" x 1706";}} @media (min-device-height:1707px){#S:after{content:" x 1707";}} @media (min-device-height:1708px){#S:after{content:" x 1708";}} @media (min-device-height:1709px){#S:after{content:" x 1709";}} @media (min-device-height:1710px){#S:after{content:" x 1710";}} @media (min-device-height:1711px){#S:after{content:" x 1711";}} @media (min-device-height:1712px){#S:after{content:" x 1712";}} @media (min-device-height:1713px){#S:after{content:" x 1713";}} @media (min-device-height:1714px){#S:after{content:" x 1714";}} @media (min-device-height:1715px){#S:after{content:" x 1715";}} @media (min-device-height:1716px){#S:after{content:" x 1716";}} @media (min-device-height:1717px){#S:after{content:" x 1717";}} @media (min-device-height:1718px){#S:after{content:" x 1718";}} @media (min-device-height:1719px){#S:after{content:" x 1719";}} @media (min-device-height:1720px){#S:after{content:" x 1720";}} @media (min-device-height:1721px){#S:after{content:" x 1721";}} @media (min-device-height:1722px){#S:after{content:" x 1722";}} @media (min-device-height:1723px){#S:after{content:" x 1723";}} @media (min-device-height:1724px){#S:after{content:" x 1724";}} @media (min-device-height:1725px){#S:after{content:" x 1725";}} @media (min-device-height:1726px){#S:after{content:" x 1726";}} @media (min-device-height:1727px){#S:after{content:" x 1727";}} @media (min-device-height:1728px){#S:after{content:" x 1728";}} @media (min-device-height:1729px){#S:after{content:" x 1729";}} @media (min-device-height:1730px){#S:after{content:" x 1730";}} @media (min-device-height:1731px){#S:after{content:" x 1731";}} @media (min-device-height:1732px){#S:after{content:" x 1732";}} @media (min-device-height:1733px){#S:after{content:" x 1733";}} @media (min-device-height:1734px){#S:after{content:" x 1734";}} @media (min-device-height:1735px){#S:after{content:" x 1735";}} @media (min-device-height:1736px){#S:after{content:" x 1736";}} @media (min-device-height:1737px){#S:after{content:" x 1737";}} @media (min-device-height:1738px){#S:after{content:" x 1738";}} @media (min-device-height:1739px){#S:after{content:" x 1739";}} @media (min-device-height:1740px){#S:after{content:" x 1740";}} @media (min-device-height:1741px){#S:after{content:" x 1741";}} @media (min-device-height:1742px){#S:after{content:" x 1742";}} @media (min-device-height:1743px){#S:after{content:" x 1743";}} @media (min-device-height:1744px){#S:after{content:" x 1744";}} @media (min-device-height:1745px){#S:after{content:" x 1745";}} @media (min-device-height:1746px){#S:after{content:" x 1746";}} @media (min-device-height:1747px){#S:after{content:" x 1747";}} @media (min-device-height:1748px){#S:after{content:" x 1748";}} @media (min-device-height:1749px){#S:after{content:" x 1749";}} @media (min-device-height:1750px){#S:after{content:" x 1750";}} @media (min-device-height:1751px){#S:after{content:" x 1751";}} @media (min-device-height:1752px){#S:after{content:" x 1752";}} @media (min-device-height:1753px){#S:after{content:" x 1753";}} @media (min-device-height:1754px){#S:after{content:" x 1754";}} @media (min-device-height:1755px){#S:after{content:" x 1755";}} @media (min-device-height:1756px){#S:after{content:" x 1756";}} @media (min-device-height:1757px){#S:after{content:" x 1757";}} @media (min-device-height:1758px){#S:after{content:" x 1758";}} @media (min-device-height:1759px){#S:after{content:" x 1759";}} @media (min-device-height:1760px){#S:after{content:" x 1760";}} @media (min-device-height:1761px){#S:after{content:" x 1761";}} @media (min-device-height:1762px){#S:after{content:" x 1762";}} @media (min-device-height:1763px){#S:after{content:" x 1763";}} @media (min-device-height:1764px){#S:after{content:" x 1764";}} @media (min-device-height:1765px){#S:after{content:" x 1765";}} @media (min-device-height:1766px){#S:after{content:" x 1766";}} @media (min-device-height:1767px){#S:after{content:" x 1767";}} @media (min-device-height:1768px){#S:after{content:" x 1768";}} @media (min-device-height:1769px){#S:after{content:" x 1769";}} @media (min-device-height:1770px){#S:after{content:" x 1770";}} @media (min-device-height:1771px){#S:after{content:" x 1771";}} @media (min-device-height:1772px){#S:after{content:" x 1772";}} @media (min-device-height:1773px){#S:after{content:" x 1773";}} @media (min-device-height:1774px){#S:after{content:" x 1774";}} @media (min-device-height:1775px){#S:after{content:" x 1775";}} @media (min-device-height:1776px){#S:after{content:" x 1776";}} @media (min-device-height:1777px){#S:after{content:" x 1777";}} @media (min-device-height:1778px){#S:after{content:" x 1778";}} @media (min-device-height:1779px){#S:after{content:" x 1779";}} @media (min-device-height:1780px){#S:after{content:" x 1780";}} @media (min-device-height:1781px){#S:after{content:" x 1781";}} @media (min-device-height:1782px){#S:after{content:" x 1782";}} @media (min-device-height:1783px){#S:after{content:" x 1783";}} @media (min-device-height:1784px){#S:after{content:" x 1784";}} @media (min-device-height:1785px){#S:after{content:" x 1785";}} @media (min-device-height:1786px){#S:after{content:" x 1786";}} @media (min-device-height:1787px){#S:after{content:" x 1787";}} @media (min-device-height:1788px){#S:after{content:" x 1788";}} @media (min-device-height:1789px){#S:after{content:" x 1789";}} @media (min-device-height:1790px){#S:after{content:" x 1790";}} @media (min-device-height:1791px){#S:after{content:" x 1791";}} @media (min-device-height:1792px){#S:after{content:" x 1792";}} @media (min-device-height:1793px){#S:after{content:" x 1793";}} @media (min-device-height:1794px){#S:after{content:" x 1794";}} @media (min-device-height:1795px){#S:after{content:" x 1795";}} @media (min-device-height:1796px){#S:after{content:" x 1796";}} @media (min-device-height:1797px){#S:after{content:" x 1797";}} @media (min-device-height:1798px){#S:after{content:" x 1798";}} @media (min-device-height:1799px){#S:after{content:" x 1799";}} @media (min-device-height:1800px){#S:after{content:" x 1800";}} @media (min-device-height:1801px){#S:after{content:" x 1801";}} @media (min-device-height:1802px){#S:after{content:" x 1802";}} @media (min-device-height:1803px){#S:after{content:" x 1803";}} @media (min-device-height:1804px){#S:after{content:" x 1804";}} @media (min-device-height:1805px){#S:after{content:" x 1805";}} @media (min-device-height:1806px){#S:after{content:" x 1806";}} @media (min-device-height:1807px){#S:after{content:" x 1807";}} @media (min-device-height:1808px){#S:after{content:" x 1808";}} @media (min-device-height:1809px){#S:after{content:" x 1809";}} @media (min-device-height:1810px){#S:after{content:" x 1810";}} @media (min-device-height:1811px){#S:after{content:" x 1811";}} @media (min-device-height:1812px){#S:after{content:" x 1812";}} @media (min-device-height:1813px){#S:after{content:" x 1813";}} @media (min-device-height:1814px){#S:after{content:" x 1814";}} @media (min-device-height:1815px){#S:after{content:" x 1815";}} @media (min-device-height:1816px){#S:after{content:" x 1816";}} @media (min-device-height:1817px){#S:after{content:" x 1817";}} @media (min-device-height:1818px){#S:after{content:" x 1818";}} @media (min-device-height:1819px){#S:after{content:" x 1819";}} @media (min-device-height:1820px){#S:after{content:" x 1820";}} @media (min-device-height:1821px){#S:after{content:" x 1821";}} @media (min-device-height:1822px){#S:after{content:" x 1822";}} @media (min-device-height:1823px){#S:after{content:" x 1823";}} @media (min-device-height:1824px){#S:after{content:" x 1824";}} @media (min-device-height:1825px){#S:after{content:" x 1825";}} @media (min-device-height:1826px){#S:after{content:" x 1826";}} @media (min-device-height:1827px){#S:after{content:" x 1827";}} @media (min-device-height:1828px){#S:after{content:" x 1828";}} @media (min-device-height:1829px){#S:after{content:" x 1829";}} @media (min-device-height:1830px){#S:after{content:" x 1830";}} @media (min-device-height:1831px){#S:after{content:" x 1831";}} @media (min-device-height:1832px){#S:after{content:" x 1832";}} @media (min-device-height:1833px){#S:after{content:" x 1833";}} @media (min-device-height:1834px){#S:after{content:" x 1834";}} @media (min-device-height:1835px){#S:after{content:" x 1835";}} @media (min-device-height:1836px){#S:after{content:" x 1836";}} @media (min-device-height:1837px){#S:after{content:" x 1837";}} @media (min-device-height:1838px){#S:after{content:" x 1838";}} @media (min-device-height:1839px){#S:after{content:" x 1839";}} @media (min-device-height:1840px){#S:after{content:" x 1840";}} @media (min-device-height:1841px){#S:after{content:" x 1841";}} @media (min-device-height:1842px){#S:after{content:" x 1842";}} @media (min-device-height:1843px){#S:after{content:" x 1843";}} @media (min-device-height:1844px){#S:after{content:" x 1844";}} @media (min-device-height:1845px){#S:after{content:" x 1845";}} @media (min-device-height:1846px){#S:after{content:" x 1846";}} @media (min-device-height:1847px){#S:after{content:" x 1847";}} @media (min-device-height:1848px){#S:after{content:" x 1848";}} @media (min-device-height:1849px){#S:after{content:" x 1849";}} @media (min-device-height:1850px){#S:after{content:" x 1850";}} @media (min-device-height:1851px){#S:after{content:" x 1851";}} @media (min-device-height:1852px){#S:after{content:" x 1852";}} @media (min-device-height:1853px){#S:after{content:" x 1853";}} @media (min-device-height:1854px){#S:after{content:" x 1854";}} @media (min-device-height:1855px){#S:after{content:" x 1855";}} @media (min-device-height:1856px){#S:after{content:" x 1856";}} @media (min-device-height:1857px){#S:after{content:" x 1857";}} @media (min-device-height:1858px){#S:after{content:" x 1858";}} @media (min-device-height:1859px){#S:after{content:" x 1859";}} @media (min-device-height:1860px){#S:after{content:" x 1860";}} @media (min-device-height:1861px){#S:after{content:" x 1861";}} @media (min-device-height:1862px){#S:after{content:" x 1862";}} @media (min-device-height:1863px){#S:after{content:" x 1863";}} @media (min-device-height:1864px){#S:after{content:" x 1864";}} @media (min-device-height:1865px){#S:after{content:" x 1865";}} @media (min-device-height:1866px){#S:after{content:" x 1866";}} @media (min-device-height:1867px){#S:after{content:" x 1867";}} @media (min-device-height:1868px){#S:after{content:" x 1868";}} @media (min-device-height:1869px){#S:after{content:" x 1869";}} @media (min-device-height:1870px){#S:after{content:" x 1870";}} @media (min-device-height:1871px){#S:after{content:" x 1871";}} @media (min-device-height:1872px){#S:after{content:" x 1872";}} @media (min-device-height:1873px){#S:after{content:" x 1873";}} @media (min-device-height:1874px){#S:after{content:" x 1874";}} @media (min-device-height:1875px){#S:after{content:" x 1875";}} @media (min-device-height:1876px){#S:after{content:" x 1876";}} @media (min-device-height:1877px){#S:after{content:" x 1877";}} @media (min-device-height:1878px){#S:after{content:" x 1878";}} @media (min-device-height:1879px){#S:after{content:" x 1879";}} @media (min-device-height:1880px){#S:after{content:" x 1880";}} @media (min-device-height:1881px){#S:after{content:" x 1881";}} @media (min-device-height:1882px){#S:after{content:" x 1882";}} @media (min-device-height:1883px){#S:after{content:" x 1883";}} @media (min-device-height:1884px){#S:after{content:" x 1884";}} @media (min-device-height:1885px){#S:after{content:" x 1885";}} @media (min-device-height:1886px){#S:after{content:" x 1886";}} @media (min-device-height:1887px){#S:after{content:" x 1887";}} @media (min-device-height:1888px){#S:after{content:" x 1888";}} @media (min-device-height:1889px){#S:after{content:" x 1889";}} @media (min-device-height:1890px){#S:after{content:" x 1890";}} @media (min-device-height:1891px){#S:after{content:" x 1891";}} @media (min-device-height:1892px){#S:after{content:" x 1892";}} @media (min-device-height:1893px){#S:after{content:" x 1893";}} @media (min-device-height:1894px){#S:after{content:" x 1894";}} @media (min-device-height:1895px){#S:after{content:" x 1895";}} @media (min-device-height:1896px){#S:after{content:" x 1896";}} @media (min-device-height:1897px){#S:after{content:" x 1897";}} @media (min-device-height:1898px){#S:after{content:" x 1898";}} @media (min-device-height:1899px){#S:after{content:" x 1899";}} @media (min-device-height:1900px){#S:after{content:" x 1900";}} @media (min-device-height:1901px){#S:after{content:" x 1901";}} @media (min-device-height:1902px){#S:after{content:" x 1902";}} @media (min-device-height:1903px){#S:after{content:" x 1903";}} @media (min-device-height:1904px){#S:after{content:" x 1904";}} @media (min-device-height:1905px){#S:after{content:" x 1905";}} @media (min-device-height:1906px){#S:after{content:" x 1906";}} @media (min-device-height:1907px){#S:after{content:" x 1907";}} @media (min-device-height:1908px){#S:after{content:" x 1908";}} @media (min-device-height:1909px){#S:after{content:" x 1909";}} @media (min-device-height:1910px){#S:after{content:" x 1910";}} @media (min-device-height:1911px){#S:after{content:" x 1911";}} @media (min-device-height:1912px){#S:after{content:" x 1912";}} @media (min-device-height:1913px){#S:after{content:" x 1913";}} @media (min-device-height:1914px){#S:after{content:" x 1914";}} @media (min-device-height:1915px){#S:after{content:" x 1915";}} @media (min-device-height:1916px){#S:after{content:" x 1916";}} @media (min-device-height:1917px){#S:after{content:" x 1917";}} @media (min-device-height:1918px){#S:after{content:" x 1918";}} @media (min-device-height:1919px){#S:after{content:" x 1919";}} @media (min-device-height:1920px){#S:after{content:" x 1920";}} @media (min-device-height:1921px){#S:after{content:" x 1921";}} @media (min-device-height:1922px){#S:after{content:" x 1922";}} @media (min-device-height:1923px){#S:after{content:" x 1923";}} @media (min-device-height:1924px){#S:after{content:" x 1924";}} @media (min-device-height:1925px){#S:after{content:" x 1925";}} @media (min-device-height:1926px){#S:after{content:" x 1926";}} @media (min-device-height:1927px){#S:after{content:" x 1927";}} @media (min-device-height:1928px){#S:after{content:" x 1928";}} @media (min-device-height:1929px){#S:after{content:" x 1929";}} @media (min-device-height:1930px){#S:after{content:" x 1930";}} @media (min-device-height:1931px){#S:after{content:" x 1931";}} @media (min-device-height:1932px){#S:after{content:" x 1932";}} @media (min-device-height:1933px){#S:after{content:" x 1933";}} @media (min-device-height:1934px){#S:after{content:" x 1934";}} @media (min-device-height:1935px){#S:after{content:" x 1935";}} @media (min-device-height:1936px){#S:after{content:" x 1936";}} @media (min-device-height:1937px){#S:after{content:" x 1937";}} @media (min-device-height:1938px){#S:after{content:" x 1938";}} @media (min-device-height:1939px){#S:after{content:" x 1939";}} @media (min-device-height:1940px){#S:after{content:" x 1940";}} @media (min-device-height:1941px){#S:after{content:" x 1941";}} @media (min-device-height:1942px){#S:after{content:" x 1942";}} @media (min-device-height:1943px){#S:after{content:" x 1943";}} @media (min-device-height:1944px){#S:after{content:" x 1944";}} @media (min-device-height:1945px){#S:after{content:" x 1945";}} @media (min-device-height:1946px){#S:after{content:" x 1946";}} @media (min-device-height:1947px){#S:after{content:" x 1947";}} @media (min-device-height:1948px){#S:after{content:" x 1948";}} @media (min-device-height:1949px){#S:after{content:" x 1949";}} @media (min-device-height:1950px){#S:after{content:" x 1950";}} @media (min-device-height:1951px){#S:after{content:" x 1951";}} @media (min-device-height:1952px){#S:after{content:" x 1952";}} @media (min-device-height:1953px){#S:after{content:" x 1953";}} @media (min-device-height:1954px){#S:after{content:" x 1954";}} @media (min-device-height:1955px){#S:after{content:" x 1955";}} @media (min-device-height:1956px){#S:after{content:" x 1956";}} @media (min-device-height:1957px){#S:after{content:" x 1957";}} @media (min-device-height:1958px){#S:after{content:" x 1958";}} @media (min-device-height:1959px){#S:after{content:" x 1959";}} @media (min-device-height:1960px){#S:after{content:" x 1960";}} @media (min-device-height:1961px){#S:after{content:" x 1961";}} @media (min-device-height:1962px){#S:after{content:" x 1962";}} @media (min-device-height:1963px){#S:after{content:" x 1963";}} @media (min-device-height:1964px){#S:after{content:" x 1964";}} @media (min-device-height:1965px){#S:after{content:" x 1965";}} @media (min-device-height:1966px){#S:after{content:" x 1966";}} @media (min-device-height:1967px){#S:after{content:" x 1967";}} @media (min-device-height:1968px){#S:after{content:" x 1968";}} @media (min-device-height:1969px){#S:after{content:" x 1969";}} @media (min-device-height:1970px){#S:after{content:" x 1970";}} @media (min-device-height:1971px){#S:after{content:" x 1971";}} @media (min-device-height:1972px){#S:after{content:" x 1972";}} @media (min-device-height:1973px){#S:after{content:" x 1973";}} @media (min-device-height:1974px){#S:after{content:" x 1974";}} @media (min-device-height:1975px){#S:after{content:" x 1975";}} @media (min-device-height:1976px){#S:after{content:" x 1976";}} @media (min-device-height:1977px){#S:after{content:" x 1977";}} @media (min-device-height:1978px){#S:after{content:" x 1978";}} @media (min-device-height:1979px){#S:after{content:" x 1979";}} @media (min-device-height:1980px){#S:after{content:" x 1980";}} @media (min-device-height:1981px){#S:after{content:" x 1981";}} @media (min-device-height:1982px){#S:after{content:" x 1982";}} @media (min-device-height:1983px){#S:after{content:" x 1983";}} @media (min-device-height:1984px){#S:after{content:" x 1984";}} @media (min-device-height:1985px){#S:after{content:" x 1985";}} @media (min-device-height:1986px){#S:after{content:" x 1986";}} @media (min-device-height:1987px){#S:after{content:" x 1987";}} @media (min-device-height:1988px){#S:after{content:" x 1988";}} @media (min-device-height:1989px){#S:after{content:" x 1989";}} @media (min-device-height:1990px){#S:after{content:" x 1990";}} @media (min-device-height:1991px){#S:after{content:" x 1991";}} @media (min-device-height:1992px){#S:after{content:" x 1992";}} @media (min-device-height:1993px){#S:after{content:" x 1993";}} @media (min-device-height:1994px){#S:after{content:" x 1994";}} @media (min-device-height:1995px){#S:after{content:" x 1995";}} @media (min-device-height:1996px){#S:after{content:" x 1996";}} @media (min-device-height:1997px){#S:after{content:" x 1997";}} @media (min-device-height:1998px){#S:after{content:" x 1998";}} @media (min-device-height:1999px){#S:after{content:" x 1999";}} @media (min-device-height:2000px){#S:after{content:" x 2000";}} @media (min-device-height:2001px){#S:after{content:" x 2001";}} @media (min-device-height:2002px){#S:after{content:" x 2002";}} @media (min-device-height:2003px){#S:after{content:" x 2003";}} @media (min-device-height:2004px){#S:after{content:" x 2004";}} @media (min-device-height:2005px){#S:after{content:" x 2005";}} @media (min-device-height:2006px){#S:after{content:" x 2006";}} @media (min-device-height:2007px){#S:after{content:" x 2007";}} @media (min-device-height:2008px){#S:after{content:" x 2008";}} @media (min-device-height:2009px){#S:after{content:" x 2009";}} @media (min-device-height:2010px){#S:after{content:" x 2010";}} @media (min-device-height:2011px){#S:after{content:" x 2011";}} @media (min-device-height:2012px){#S:after{content:" x 2012";}} @media (min-device-height:2013px){#S:after{content:" x 2013";}} @media (min-device-height:2014px){#S:after{content:" x 2014";}} @media (min-device-height:2015px){#S:after{content:" x 2015";}} @media (min-device-height:2016px){#S:after{content:" x 2016";}} @media (min-device-height:2017px){#S:after{content:" x 2017";}} @media (min-device-height:2018px){#S:after{content:" x 2018";}} @media (min-device-height:2019px){#S:after{content:" x 2019";}} @media (min-device-height:2020px){#S:after{content:" x 2020";}} @media (min-device-height:2021px){#S:after{content:" x 2021";}} @media (min-device-height:2022px){#S:after{content:" x 2022";}} @media (min-device-height:2023px){#S:after{content:" x 2023";}} @media (min-device-height:2024px){#S:after{content:" x 2024";}} @media (min-device-height:2025px){#S:after{content:" x 2025";}} @media (min-device-height:2026px){#S:after{content:" x 2026";}} @media (min-device-height:2027px){#S:after{content:" x 2027";}} @media (min-device-height:2028px){#S:after{content:" x 2028";}} @media (min-device-height:2029px){#S:after{content:" x 2029";}} @media (min-device-height:2030px){#S:after{content:" x 2030";}} @media (min-device-height:2031px){#S:after{content:" x 2031";}} @media (min-device-height:2032px){#S:after{content:" x 2032";}} @media (min-device-height:2033px){#S:after{content:" x 2033";}} @media (min-device-height:2034px){#S:after{content:" x 2034";}} @media (min-device-height:2035px){#S:after{content:" x 2035";}} @media (min-device-height:2036px){#S:after{content:" x 2036";}} @media (min-device-height:2037px){#S:after{content:" x 2037";}} @media (min-device-height:2038px){#S:after{content:" x 2038";}} @media (min-device-height:2039px){#S:after{content:" x 2039";}} @media (min-device-height:2040px){#S:after{content:" x 2040";}} @media (min-device-height:2041px){#S:after{content:" x 2041";}} @media (min-device-height:2042px){#S:after{content:" x 2042";}} @media (min-device-height:2043px){#S:after{content:" x 2043";}} @media (min-device-height:2044px){#S:after{content:" x 2044";}} @media (min-device-height:2045px){#S:after{content:" x 2045";}} @media (min-device-height:2046px){#S:after{content:" x 2046";}} @media (min-device-height:2047px){#S:after{content:" x 2047";}} @media (min-device-height:2048px){#S:after{content:" x 2048";}} @media (min-device-height:2049px){#S:after{content:" x 2049";}} @media (min-device-height:2050px){#S:after{content:" x 2050";}} @media (min-device-height:2051px){#S:after{content:" x 2051";}} @media (min-device-height:2052px){#S:after{content:" x 2052";}} @media (min-device-height:2053px){#S:after{content:" x 2053";}} @media (min-device-height:2054px){#S:after{content:" x 2054";}} @media (min-device-height:2055px){#S:after{content:" x 2055";}} @media (min-device-height:2056px){#S:after{content:" x 2056";}} @media (min-device-height:2057px){#S:after{content:" x 2057";}} @media (min-device-height:2058px){#S:after{content:" x 2058";}} @media (min-device-height:2059px){#S:after{content:" x 2059";}} @media (min-device-height:2060px){#S:after{content:" x 2060";}} @media (min-device-height:2061px){#S:after{content:" x 2061";}} @media (min-device-height:2062px){#S:after{content:" x 2062";}} @media (min-device-height:2063px){#S:after{content:" x 2063";}} @media (min-device-height:2064px){#S:after{content:" x 2064";}} @media (min-device-height:2065px){#S:after{content:" x 2065";}} @media (min-device-height:2066px){#S:after{content:" x 2066";}} @media (min-device-height:2067px){#S:after{content:" x 2067";}} @media (min-device-height:2068px){#S:after{content:" x 2068";}} @media (min-device-height:2069px){#S:after{content:" x 2069";}} @media (min-device-height:2070px){#S:after{content:" x 2070";}} @media (min-device-height:2071px){#S:after{content:" x 2071";}} @media (min-device-height:2072px){#S:after{content:" x 2072";}} @media (min-device-height:2073px){#S:after{content:" x 2073";}} @media (min-device-height:2074px){#S:after{content:" x 2074";}} @media (min-device-height:2075px){#S:after{content:" x 2075";}} @media (min-device-height:2076px){#S:after{content:" x 2076";}} @media (min-device-height:2077px){#S:after{content:" x 2077";}} @media (min-device-height:2078px){#S:after{content:" x 2078";}} @media (min-device-height:2079px){#S:after{content:" x 2079";}} @media (min-device-height:2080px){#S:after{content:" x 2080";}} @media (min-device-height:2081px){#S:after{content:" x 2081";}} @media (min-device-height:2082px){#S:after{content:" x 2082";}} @media (min-device-height:2083px){#S:after{content:" x 2083";}} @media (min-device-height:2084px){#S:after{content:" x 2084";}} @media (min-device-height:2085px){#S:after{content:" x 2085";}} @media (min-device-height:2086px){#S:after{content:" x 2086";}} @media (min-device-height:2087px){#S:after{content:" x 2087";}} @media (min-device-height:2088px){#S:after{content:" x 2088";}} @media (min-device-height:2089px){#S:after{content:" x 2089";}} @media (min-device-height:2090px){#S:after{content:" x 2090";}} @media (min-device-height:2091px){#S:after{content:" x 2091";}} @media (min-device-height:2092px){#S:after{content:" x 2092";}} @media (min-device-height:2093px){#S:after{content:" x 2093";}} @media (min-device-height:2094px){#S:after{content:" x 2094";}} @media (min-device-height:2095px){#S:after{content:" x 2095";}} @media (min-device-height:2096px){#S:after{content:" x 2096";}} @media (min-device-height:2097px){#S:after{content:" x 2097";}} @media (min-device-height:2098px){#S:after{content:" x 2098";}} @media (min-device-height:2099px){#S:after{content:" x 2099";}} @media (min-device-height:2100px){#S:after{content:" x 2100";}} @media (min-device-height:2101px){#S:after{content:" x 2101";}} @media (min-device-height:2102px){#S:after{content:" x 2102";}} @media (min-device-height:2103px){#S:after{content:" x 2103";}} @media (min-device-height:2104px){#S:after{content:" x 2104";}} @media (min-device-height:2105px){#S:after{content:" x 2105";}} @media (min-device-height:2106px){#S:after{content:" x 2106";}} @media (min-device-height:2107px){#S:after{content:" x 2107";}} @media (min-device-height:2108px){#S:after{content:" x 2108";}} @media (min-device-height:2109px){#S:after{content:" x 2109";}} @media (min-device-height:2110px){#S:after{content:" x 2110";}} @media (min-device-height:2111px){#S:after{content:" x 2111";}} @media (min-device-height:2112px){#S:after{content:" x 2112";}} @media (min-device-height:2113px){#S:after{content:" x 2113";}} @media (min-device-height:2114px){#S:after{content:" x 2114";}} @media (min-device-height:2115px){#S:after{content:" x 2115";}} @media (min-device-height:2116px){#S:after{content:" x 2116";}} @media (min-device-height:2117px){#S:after{content:" x 2117";}} @media (min-device-height:2118px){#S:after{content:" x 2118";}} @media (min-device-height:2119px){#S:after{content:" x 2119";}} @media (min-device-height:2120px){#S:after{content:" x 2120";}} @media (min-device-height:2121px){#S:after{content:" x 2121";}} @media (min-device-height:2122px){#S:after{content:" x 2122";}} @media (min-device-height:2123px){#S:after{content:" x 2123";}} @media (min-device-height:2124px){#S:after{content:" x 2124";}} @media (min-device-height:2125px){#S:after{content:" x 2125";}} @media (min-device-height:2126px){#S:after{content:" x 2126";}} @media (min-device-height:2127px){#S:after{content:" x 2127";}} @media (min-device-height:2128px){#S:after{content:" x 2128";}} @media (min-device-height:2129px){#S:after{content:" x 2129";}} @media (min-device-height:2130px){#S:after{content:" x 2130";}} @media (min-device-height:2131px){#S:after{content:" x 2131";}} @media (min-device-height:2132px){#S:after{content:" x 2132";}} @media (min-device-height:2133px){#S:after{content:" x 2133";}} @media (min-device-height:2134px){#S:after{content:" x 2134";}} @media (min-device-height:2135px){#S:after{content:" x 2135";}} @media (min-device-height:2136px){#S:after{content:" x 2136";}} @media (min-device-height:2137px){#S:after{content:" x 2137";}} @media (min-device-height:2138px){#S:after{content:" x 2138";}} @media (min-device-height:2139px){#S:after{content:" x 2139";}} @media (min-device-height:2140px){#S:after{content:" x 2140";}} @media (min-device-height:2141px){#S:after{content:" x 2141";}} @media (min-device-height:2142px){#S:after{content:" x 2142";}} @media (min-device-height:2143px){#S:after{content:" x 2143";}} @media (min-device-height:2144px){#S:after{content:" x 2144";}} @media (min-device-height:2145px){#S:after{content:" x 2145";}} @media (min-device-height:2146px){#S:after{content:" x 2146";}} @media (min-device-height:2147px){#S:after{content:" x 2147";}} @media (min-device-height:2148px){#S:after{content:" x 2148";}} @media (min-device-height:2149px){#S:after{content:" x 2149";}} @media (min-device-height:2150px){#S:after{content:" x 2150";}} @media (min-device-height:2151px){#S:after{content:" x 2151";}} @media (min-device-height:2152px){#S:after{content:" x 2152";}} @media (min-device-height:2153px){#S:after{content:" x 2153";}} @media (min-device-height:2154px){#S:after{content:" x 2154";}} @media (min-device-height:2155px){#S:after{content:" x 2155";}} @media (min-device-height:2156px){#S:after{content:" x 2156";}} @media (min-device-height:2157px){#S:after{content:" x 2157";}} @media (min-device-height:2158px){#S:after{content:" x 2158";}} @media (min-device-height:2159px){#S:after{content:" x 2159";}} @media (min-device-height:2160px){#S:after{content:" x 2160";}} @media (min-device-height:2161px){#S:after{content:" x 2161";}} @media (min-device-height:2162px){#S:after{content:" x 2162";}} @media (min-device-height:2163px){#S:after{content:" x 2163";}} @media (min-device-height:2164px){#S:after{content:" x 2164";}} @media (min-device-height:2165px){#S:after{content:" x 2165";}} @media (min-device-height:2166px){#S:after{content:" x 2166";}} @media (min-device-height:2167px){#S:after{content:" x 2167";}} @media (min-device-height:2168px){#S:after{content:" x 2168";}} @media (min-device-height:2169px){#S:after{content:" x 2169";}} @media (min-device-height:2170px){#S:after{content:" x 2170";}} @media (min-device-height:2171px){#S:after{content:" x 2171";}} @media (min-device-height:2172px){#S:after{content:" x 2172";}} @media (min-device-height:2173px){#S:after{content:" x 2173";}} @media (min-device-height:2174px){#S:after{content:" x 2174";}} @media (min-device-height:2175px){#S:after{content:" x 2175";}} @media (min-device-height:2176px){#S:after{content:" x 2176";}} @media (min-device-height:2177px){#S:after{content:" x 2177";}} @media (min-device-height:2178px){#S:after{content:" x 2178";}} @media (min-device-height:2179px){#S:after{content:" x 2179";}} @media (min-device-height:2180px){#S:after{content:" x 2180";}} @media (min-device-height:2181px){#S:after{content:" x 2181";}} @media (min-device-height:2182px){#S:after{content:" x 2182";}} @media (min-device-height:2183px){#S:after{content:" x 2183";}} @media (min-device-height:2184px){#S:after{content:" x 2184";}} @media (min-device-height:2185px){#S:after{content:" x 2185";}} @media (min-device-height:2186px){#S:after{content:" x 2186";}} @media (min-device-height:2187px){#S:after{content:" x 2187";}} @media (min-device-height:2188px){#S:after{content:" x 2188";}} @media (min-device-height:2189px){#S:after{content:" x 2189";}} @media (min-device-height:2190px){#S:after{content:" x 2190";}} @media (min-device-height:2191px){#S:after{content:" x 2191";}} @media (min-device-height:2192px){#S:after{content:" x 2192";}} @media (min-device-height:2193px){#S:after{content:" x 2193";}} @media (min-device-height:2194px){#S:after{content:" x 2194";}} @media (min-device-height:2195px){#S:after{content:" x 2195";}} @media (min-device-height:2196px){#S:after{content:" x 2196";}} @media (min-device-height:2197px){#S:after{content:" x 2197";}} @media (min-device-height:2198px){#S:after{content:" x 2198";}} @media (min-device-height:2199px){#S:after{content:" x 2199";}} @media (min-device-height:2200px){#S:after{content:" x 2200";}} @media (min-device-height:2201px){#S:after{content:" x 2201";}} @media (min-device-height:2202px){#S:after{content:" x 2202";}} @media (min-device-height:2203px){#S:after{content:" x 2203";}} @media (min-device-height:2204px){#S:after{content:" x 2204";}} @media (min-device-height:2205px){#S:after{content:" x 2205";}} @media (min-device-height:2206px){#S:after{content:" x 2206";}} @media (min-device-height:2207px){#S:after{content:" x 2207";}} @media (min-device-height:2208px){#S:after{content:" x 2208";}} @media (min-device-height:2209px){#S:after{content:" x 2209";}} @media (min-device-height:2210px){#S:after{content:" x 2210";}} @media (min-device-height:2211px){#S:after{content:" x 2211";}} @media (min-device-height:2212px){#S:after{content:" x 2212";}} @media (min-device-height:2213px){#S:after{content:" x 2213";}} @media (min-device-height:2214px){#S:after{content:" x 2214";}} @media (min-device-height:2215px){#S:after{content:" x 2215";}} @media (min-device-height:2216px){#S:after{content:" x 2216";}} @media (min-device-height:2217px){#S:after{content:" x 2217";}} @media (min-device-height:2218px){#S:after{content:" x 2218";}} @media (min-device-height:2219px){#S:after{content:" x 2219";}} @media (min-device-height:2220px){#S:after{content:" x 2220";}} @media (min-device-height:2221px){#S:after{content:" x 2221";}} @media (min-device-height:2222px){#S:after{content:" x 2222";}} @media (min-device-height:2223px){#S:after{content:" x 2223";}} @media (min-device-height:2224px){#S:after{content:" x 2224";}} @media (min-device-height:2225px){#S:after{content:" x 2225";}} @media (min-device-height:2226px){#S:after{content:" x 2226";}} @media (min-device-height:2227px){#S:after{content:" x 2227";}} @media (min-device-height:2228px){#S:after{content:" x 2228";}} @media (min-device-height:2229px){#S:after{content:" x 2229";}} @media (min-device-height:2230px){#S:after{content:" x 2230";}} @media (min-device-height:2231px){#S:after{content:" x 2231";}} @media (min-device-height:2232px){#S:after{content:" x 2232";}} @media (min-device-height:2233px){#S:after{content:" x 2233";}} @media (min-device-height:2234px){#S:after{content:" x 2234";}} @media (min-device-height:2235px){#S:after{content:" x 2235";}} @media (min-device-height:2236px){#S:after{content:" x 2236";}} @media (min-device-height:2237px){#S:after{content:" x 2237";}} @media (min-device-height:2238px){#S:after{content:" x 2238";}} @media (min-device-height:2239px){#S:after{content:" x 2239";}} @media (min-device-height:2240px){#S:after{content:" x 2240";}} @media (min-device-height:2241px){#S:after{content:" x 2241";}} @media (min-device-height:2242px){#S:after{content:" x 2242";}} @media (min-device-height:2243px){#S:after{content:" x 2243";}} @media (min-device-height:2244px){#S:after{content:" x 2244";}} @media (min-device-height:2245px){#S:after{content:" x 2245";}} @media (min-device-height:2246px){#S:after{content:" x 2246";}} @media (min-device-height:2247px){#S:after{content:" x 2247";}} @media (min-device-height:2248px){#S:after{content:" x 2248";}} @media (min-device-height:2249px){#S:after{content:" x 2249";}} @media (min-device-height:2250px){#S:after{content:" x 2250";}} @media (min-device-height:2251px){#S:after{content:" x 2251";}} @media (min-device-height:2252px){#S:after{content:" x 2252";}} @media (min-device-height:2253px){#S:after{content:" x 2253";}} @media (min-device-height:2254px){#S:after{content:" x 2254";}} @media (min-device-height:2255px){#S:after{content:" x 2255";}} @media (min-device-height:2256px){#S:after{content:" x 2256";}} @media (min-device-height:2257px){#S:after{content:" x 2257";}} @media (min-device-height:2258px){#S:after{content:" x 2258";}} @media (min-device-height:2259px){#S:after{content:" x 2259";}} @media (min-device-height:2260px){#S:after{content:" x 2260";}} @media (min-device-height:2261px){#S:after{content:" x 2261";}} @media (min-device-height:2262px){#S:after{content:" x 2262";}} @media (min-device-height:2263px){#S:after{content:" x 2263";}} @media (min-device-height:2264px){#S:after{content:" x 2264";}} @media (min-device-height:2265px){#S:after{content:" x 2265";}} @media (min-device-height:2266px){#S:after{content:" x 2266";}} @media (min-device-height:2267px){#S:after{content:" x 2267";}} @media (min-device-height:2268px){#S:after{content:" x 2268";}} @media (min-device-height:2269px){#S:after{content:" x 2269";}} @media (min-device-height:2270px){#S:after{content:" x 2270";}} @media (min-device-height:2271px){#S:after{content:" x 2271";}} @media (min-device-height:2272px){#S:after{content:" x 2272";}} @media (min-device-height:2273px){#S:after{content:" x 2273";}} @media (min-device-height:2274px){#S:after{content:" x 2274";}} @media (min-device-height:2275px){#S:after{content:" x 2275";}} @media (min-device-height:2276px){#S:after{content:" x 2276";}} @media (min-device-height:2277px){#S:after{content:" x 2277";}} @media (min-device-height:2278px){#S:after{content:" x 2278";}} @media (min-device-height:2279px){#S:after{content:" x 2279";}} @media (min-device-height:2280px){#S:after{content:" x 2280";}} @media (min-device-height:2281px){#S:after{content:" x 2281";}} @media (min-device-height:2282px){#S:after{content:" x 2282";}} @media (min-device-height:2283px){#S:after{content:" x 2283";}} @media (min-device-height:2284px){#S:after{content:" x 2284";}} @media (min-device-height:2285px){#S:after{content:" x 2285";}} @media (min-device-height:2286px){#S:after{content:" x 2286";}} @media (min-device-height:2287px){#S:after{content:" x 2287";}} @media (min-device-height:2288px){#S:after{content:" x 2288";}} @media (min-device-height:2289px){#S:after{content:" x 2289";}} @media (min-device-height:2290px){#S:after{content:" x 2290";}} @media (min-device-height:2291px){#S:after{content:" x 2291";}} @media (min-device-height:2292px){#S:after{content:" x 2292";}} @media (min-device-height:2293px){#S:after{content:" x 2293";}} @media (min-device-height:2294px){#S:after{content:" x 2294";}} @media (min-device-height:2295px){#S:after{content:" x 2295";}} @media (min-device-height:2296px){#S:after{content:" x 2296";}} @media (min-device-height:2297px){#S:after{content:" x 2297";}} @media (min-device-height:2298px){#S:after{content:" x 2298";}} @media (min-device-height:2299px){#S:after{content:" x 2299";}} @media (min-device-height:2300px){#S:after{content:" x 2300";}} @media (min-device-height:2301px){#S:after{content:" x 2301";}} @media (min-device-height:2302px){#S:after{content:" x 2302";}} @media (min-device-height:2303px){#S:after{content:" x 2303";}} @media (min-device-height:2304px){#S:after{content:" x 2304";}} @media (min-device-height:2305px){#S:after{content:" x 2305";}} @media (min-device-height:2306px){#S:after{content:" x 2306";}} @media (min-device-height:2307px){#S:after{content:" x 2307";}} @media (min-device-height:2308px){#S:after{content:" x 2308";}} @media (min-device-height:2309px){#S:after{content:" x 2309";}} @media (min-device-height:2310px){#S:after{content:" x 2310";}} @media (min-device-height:2311px){#S:after{content:" x 2311";}} @media (min-device-height:2312px){#S:after{content:" x 2312";}} @media (min-device-height:2313px){#S:after{content:" x 2313";}} @media (min-device-height:2314px){#S:after{content:" x 2314";}} @media (min-device-height:2315px){#S:after{content:" x 2315";}} @media (min-device-height:2316px){#S:after{content:" x 2316";}} @media (min-device-height:2317px){#S:after{content:" x 2317";}} @media (min-device-height:2318px){#S:after{content:" x 2318";}} @media (min-device-height:2319px){#S:after{content:" x 2319";}} @media (min-device-height:2320px){#S:after{content:" x 2320";}} @media (min-device-height:2321px){#S:after{content:" x 2321";}} @media (min-device-height:2322px){#S:after{content:" x 2322";}} @media (min-device-height:2323px){#S:after{content:" x 2323";}} @media (min-device-height:2324px){#S:after{content:" x 2324";}} @media (min-device-height:2325px){#S:after{content:" x 2325";}} @media (min-device-height:2326px){#S:after{content:" x 2326";}} @media (min-device-height:2327px){#S:after{content:" x 2327";}} @media (min-device-height:2328px){#S:after{content:" x 2328";}} @media (min-device-height:2329px){#S:after{content:" x 2329";}} @media (min-device-height:2330px){#S:after{content:" x 2330";}} @media (min-device-height:2331px){#S:after{content:" x 2331";}} @media (min-device-height:2332px){#S:after{content:" x 2332";}} @media (min-device-height:2333px){#S:after{content:" x 2333";}} @media (min-device-height:2334px){#S:after{content:" x 2334";}} @media (min-device-height:2335px){#S:after{content:" x 2335";}} @media (min-device-height:2336px){#S:after{content:" x 2336";}} @media (min-device-height:2337px){#S:after{content:" x 2337";}} @media (min-device-height:2338px){#S:after{content:" x 2338";}} @media (min-device-height:2339px){#S:after{content:" x 2339";}} @media (min-device-height:2340px){#S:after{content:" x 2340";}} @media (min-device-height:2341px){#S:after{content:" x 2341";}} @media (min-device-height:2342px){#S:after{content:" x 2342";}} @media (min-device-height:2343px){#S:after{content:" x 2343";}} @media (min-device-height:2344px){#S:after{content:" x 2344";}} @media (min-device-height:2345px){#S:after{content:" x 2345";}} @media (min-device-height:2346px){#S:after{content:" x 2346";}} @media (min-device-height:2347px){#S:after{content:" x 2347";}} @media (min-device-height:2348px){#S:after{content:" x 2348";}} @media (min-device-height:2349px){#S:after{content:" x 2349";}} @media (min-device-height:2350px){#S:after{content:" x 2350";}} @media (min-device-height:2351px){#S:after{content:" x 2351";}} @media (min-device-height:2352px){#S:after{content:" x 2352";}} @media (min-device-height:2353px){#S:after{content:" x 2353";}} @media (min-device-height:2354px){#S:after{content:" x 2354";}} @media (min-device-height:2355px){#S:after{content:" x 2355";}} @media (min-device-height:2356px){#S:after{content:" x 2356";}} @media (min-device-height:2357px){#S:after{content:" x 2357";}} @media (min-device-height:2358px){#S:after{content:" x 2358";}} @media (min-device-height:2359px){#S:after{content:" x 2359";}} @media (min-device-height:2360px){#S:after{content:" x 2360";}} @media (min-device-height:2361px){#S:after{content:" x 2361";}} @media (min-device-height:2362px){#S:after{content:" x 2362";}} @media (min-device-height:2363px){#S:after{content:" x 2363";}} @media (min-device-height:2364px){#S:after{content:" x 2364";}} @media (min-device-height:2365px){#S:after{content:" x 2365";}} @media (min-device-height:2366px){#S:after{content:" x 2366";}} @media (min-device-height:2367px){#S:after{content:" x 2367";}} @media (min-device-height:2368px){#S:after{content:" x 2368";}} @media (min-device-height:2369px){#S:after{content:" x 2369";}} @media (min-device-height:2370px){#S:after{content:" x 2370";}} @media (min-device-height:2371px){#S:after{content:" x 2371";}} @media (min-device-height:2372px){#S:after{content:" x 2372";}} @media (min-device-height:2373px){#S:after{content:" x 2373";}} @media (min-device-height:2374px){#S:after{content:" x 2374";}} @media (min-device-height:2375px){#S:after{content:" x 2375";}} @media (min-device-height:2376px){#S:after{content:" x 2376";}} @media (min-device-height:2377px){#S:after{content:" x 2377";}} @media (min-device-height:2378px){#S:after{content:" x 2378";}} @media (min-device-height:2379px){#S:after{content:" x 2379";}} @media (min-device-height:2380px){#S:after{content:" x 2380";}} @media (min-device-height:2381px){#S:after{content:" x 2381";}} @media (min-device-height:2382px){#S:after{content:" x 2382";}} @media (min-device-height:2383px){#S:after{content:" x 2383";}} @media (min-device-height:2384px){#S:after{content:" x 2384";}} @media (min-device-height:2385px){#S:after{content:" x 2385";}} @media (min-device-height:2386px){#S:after{content:" x 2386";}} @media (min-device-height:2387px){#S:after{content:" x 2387";}} @media (min-device-height:2388px){#S:after{content:" x 2388";}} @media (min-device-height:2389px){#S:after{content:" x 2389";}} @media (min-device-height:2390px){#S:after{content:" x 2390";}} @media (min-device-height:2391px){#S:after{content:" x 2391";}} @media (min-device-height:2392px){#S:after{content:" x 2392";}} @media (min-device-height:2393px){#S:after{content:" x 2393";}} @media (min-device-height:2394px){#S:after{content:" x 2394";}} @media (min-device-height:2395px){#S:after{content:" x 2395";}} @media (min-device-height:2396px){#S:after{content:" x 2396";}} @media (min-device-height:2397px){#S:after{content:" x 2397";}} @media (min-device-height:2398px){#S:after{content:" x 2398";}} @media (min-device-height:2399px){#S:after{content:" x 2399";}} @media (min-device-height:2400px){#S:after{content:" x 2400";}} @media (min-device-height:2401px){#S:after{content:" x 2401";}} @media (min-device-height:2402px){#S:after{content:" x 2402";}} @media (min-device-height:2403px){#S:after{content:" x 2403";}} @media (min-device-height:2404px){#S:after{content:" x 2404";}} @media (min-device-height:2405px){#S:after{content:" x 2405";}} @media (min-device-height:2406px){#S:after{content:" x 2406";}} @media (min-device-height:2407px){#S:after{content:" x 2407";}} @media (min-device-height:2408px){#S:after{content:" x 2408";}} @media (min-device-height:2409px){#S:after{content:" x 2409";}} @media (min-device-height:2410px){#S:after{content:" x 2410";}} @media (min-device-height:2411px){#S:after{content:" x 2411";}} @media (min-device-height:2412px){#S:after{content:" x 2412";}} @media (min-device-height:2413px){#S:after{content:" x 2413";}} @media (min-device-height:2414px){#S:after{content:" x 2414";}} @media (min-device-height:2415px){#S:after{content:" x 2415";}} @media (min-device-height:2416px){#S:after{content:" x 2416";}} @media (min-device-height:2417px){#S:after{content:" x 2417";}} @media (min-device-height:2418px){#S:after{content:" x 2418";}} @media (min-device-height:2419px){#S:after{content:" x 2419";}} @media (min-device-height:2420px){#S:after{content:" x 2420";}} @media (min-device-height:2421px){#S:after{content:" x 2421";}} @media (min-device-height:2422px){#S:after{content:" x 2422";}} @media (min-device-height:2423px){#S:after{content:" x 2423";}} @media (min-device-height:2424px){#S:after{content:" x 2424";}} @media (min-device-height:2425px){#S:after{content:" x 2425";}} @media (min-device-height:2426px){#S:after{content:" x 2426";}} @media (min-device-height:2427px){#S:after{content:" x 2427";}} @media (min-device-height:2428px){#S:after{content:" x 2428";}} @media (min-device-height:2429px){#S:after{content:" x 2429";}} @media (min-device-height:2430px){#S:after{content:" x 2430";}} @media (min-device-height:2431px){#S:after{content:" x 2431";}} @media (min-device-height:2432px){#S:after{content:" x 2432";}} @media (min-device-height:2433px){#S:after{content:" x 2433";}} @media (min-device-height:2434px){#S:after{content:" x 2434";}} @media (min-device-height:2435px){#S:after{content:" x 2435";}} @media (min-device-height:2436px){#S:after{content:" x 2436";}} @media (min-device-height:2437px){#S:after{content:" x 2437";}} @media (min-device-height:2438px){#S:after{content:" x 2438";}} @media (min-device-height:2439px){#S:after{content:" x 2439";}} @media (min-device-height:2440px){#S:after{content:" x 2440";}} @media (min-device-height:2441px){#S:after{content:" x 2441";}} @media (min-device-height:2442px){#S:after{content:" x 2442";}} @media (min-device-height:2443px){#S:after{content:" x 2443";}} @media (min-device-height:2444px){#S:after{content:" x 2444";}} @media (min-device-height:2445px){#S:after{content:" x 2445";}} @media (min-device-height:2446px){#S:after{content:" x 2446";}} @media (min-device-height:2447px){#S:after{content:" x 2447";}} @media (min-device-height:2448px){#S:after{content:" x 2448";}} @media (min-device-height:2449px){#S:after{content:" x 2449";}} @media (min-device-height:2450px){#S:after{content:" x 2450";}} @media (min-device-height:2451px){#S:after{content:" x 2451";}} @media (min-device-height:2452px){#S:after{content:" x 2452";}} @media (min-device-height:2453px){#S:after{content:" x 2453";}} @media (min-device-height:2454px){#S:after{content:" x 2454";}} @media (min-device-height:2455px){#S:after{content:" x 2455";}} @media (min-device-height:2456px){#S:after{content:" x 2456";}} @media (min-device-height:2457px){#S:after{content:" x 2457";}} @media (min-device-height:2458px){#S:after{content:" x 2458";}} @media (min-device-height:2459px){#S:after{content:" x 2459";}} @media (min-device-height:2460px){#S:after{content:" x 2460";}} @media (min-device-height:2461px){#S:after{content:" x 2461";}} @media (min-device-height:2462px){#S:after{content:" x 2462";}} @media (min-device-height:2463px){#S:after{content:" x 2463";}} @media (min-device-height:2464px){#S:after{content:" x 2464";}} @media (min-device-height:2465px){#S:after{content:" x 2465";}} @media (min-device-height:2466px){#S:after{content:" x 2466";}} @media (min-device-height:2467px){#S:after{content:" x 2467";}} @media (min-device-height:2468px){#S:after{content:" x 2468";}} @media (min-device-height:2469px){#S:after{content:" x 2469";}} @media (min-device-height:2470px){#S:after{content:" x 2470";}} @media (min-device-height:2471px){#S:after{content:" x 2471";}} @media (min-device-height:2472px){#S:after{content:" x 2472";}} @media (min-device-height:2473px){#S:after{content:" x 2473";}} @media (min-device-height:2474px){#S:after{content:" x 2474";}} @media (min-device-height:2475px){#S:after{content:" x 2475";}} @media (min-device-height:2476px){#S:after{content:" x 2476";}} @media (min-device-height:2477px){#S:after{content:" x 2477";}} @media (min-device-height:2478px){#S:after{content:" x 2478";}} @media (min-device-height:2479px){#S:after{content:" x 2479";}} @media (min-device-height:2480px){#S:after{content:" x 2480";}} @media (min-device-height:2481px){#S:after{content:" x 2481";}} @media (min-device-height:2482px){#S:after{content:" x 2482";}} @media (min-device-height:2483px){#S:after{content:" x 2483";}} @media (min-device-height:2484px){#S:after{content:" x 2484";}} @media (min-device-height:2485px){#S:after{content:" x 2485";}} @media (min-device-height:2486px){#S:after{content:" x 2486";}} @media (min-device-height:2487px){#S:after{content:" x 2487";}} @media (min-device-height:2488px){#S:after{content:" x 2488";}} @media (min-device-height:2489px){#S:after{content:" x 2489";}} @media (min-device-height:2490px){#S:after{content:" x 2490";}} @media (min-device-height:2491px){#S:after{content:" x 2491";}} @media (min-device-height:2492px){#S:after{content:" x 2492";}} @media (min-device-height:2493px){#S:after{content:" x 2493";}} @media (min-device-height:2494px){#S:after{content:" x 2494";}} @media (min-device-height:2495px){#S:after{content:" x 2495";}} @media (min-device-height:2496px){#S:after{content:" x 2496";}} @media (min-device-height:2497px){#S:after{content:" x 2497";}} @media (min-device-height:2498px){#S:after{content:" x 2498";}} @media (min-device-height:2499px){#S:after{content:" x 2499";}} @media (min-device-height:2500px){#S:after{content:" x 2500";}} @media (min-device-height:2501px){#S:after{content:" x 2501";}} @media (min-device-height:2502px){#S:after{content:" x 2502";}} @media (min-device-height:2503px){#S:after{content:" x 2503";}} @media (min-device-height:2504px){#S:after{content:" x 2504";}} @media (min-device-height:2505px){#S:after{content:" x 2505";}} @media (min-device-height:2506px){#S:after{content:" x 2506";}} @media (min-device-height:2507px){#S:after{content:" x 2507";}} @media (min-device-height:2508px){#S:after{content:" x 2508";}} @media (min-device-height:2509px){#S:after{content:" x 2509";}} @media (min-device-height:2510px){#S:after{content:" x 2510";}} @media (min-device-height:2511px){#S:after{content:" x 2511";}} @media (min-device-height:2512px){#S:after{content:" x 2512";}} @media (min-device-height:2513px){#S:after{content:" x 2513";}} @media (min-device-height:2514px){#S:after{content:" x 2514";}} @media (min-device-height:2515px){#S:after{content:" x 2515";}} @media (min-device-height:2516px){#S:after{content:" x 2516";}} @media (min-device-height:2517px){#S:after{content:" x 2517";}} @media (min-device-height:2518px){#S:after{content:" x 2518";}} @media (min-device-height:2519px){#S:after{content:" x 2519";}} @media (min-device-height:2520px){#S:after{content:" x 2520";}} @media (min-device-height:2521px){#S:after{content:" x 2521";}} @media (min-device-height:2522px){#S:after{content:" x 2522";}} @media (min-device-height:2523px){#S:after{content:" x 2523";}} @media (min-device-height:2524px){#S:after{content:" x 2524";}} @media (min-device-height:2525px){#S:after{content:" x 2525";}} @media (min-device-height:2526px){#S:after{content:" x 2526";}} @media (min-device-height:2527px){#S:after{content:" x 2527";}} @media (min-device-height:2528px){#S:after{content:" x 2528";}} @media (min-device-height:2529px){#S:after{content:" x 2529";}} @media (min-device-height:2530px){#S:after{content:" x 2530";}} @media (min-device-height:2531px){#S:after{content:" x 2531";}} @media (min-device-height:2532px){#S:after{content:" x 2532";}} @media (min-device-height:2533px){#S:after{content:" x 2533";}} @media (min-device-height:2534px){#S:after{content:" x 2534";}} @media (min-device-height:2535px){#S:after{content:" x 2535";}} @media (min-device-height:2536px){#S:after{content:" x 2536";}} @media (min-device-height:2537px){#S:after{content:" x 2537";}} @media (min-device-height:2538px){#S:after{content:" x 2538";}} @media (min-device-height:2539px){#S:after{content:" x 2539";}} @media (min-device-height:2540px){#S:after{content:" x 2540";}} @media (min-device-height:2541px){#S:after{content:" x 2541";}} @media (min-device-height:2542px){#S:after{content:" x 2542";}} @media (min-device-height:2543px){#S:after{content:" x 2543";}} @media (min-device-height:2544px){#S:after{content:" x 2544";}} @media (min-device-height:2545px){#S:after{content:" x 2545";}} @media (min-device-height:2546px){#S:after{content:" x 2546";}} @media (min-device-height:2547px){#S:after{content:" x 2547";}} @media (min-device-height:2548px){#S:after{content:" x 2548";}} @media (min-device-height:2549px){#S:after{content:" x 2549";}} @media (min-device-height:2550px){#S:after{content:" x 2550";}} @media (min-device-height:2551px){#S:after{content:" x 2551";}} @media (min-device-height:2552px){#S:after{content:" x 2552";}} @media (min-device-height:2553px){#S:after{content:" x 2553";}} @media (min-device-height:2554px){#S:after{content:" x 2554";}} @media (min-device-height:2555px){#S:after{content:" x 2555";}} @media (min-device-height:2556px){#S:after{content:" x 2556";}} @media (min-device-height:2557px){#S:after{content:" x 2557";}} @media (min-device-height:2558px){#S:after{content:" x 2558";}} @media (min-device-height:2559px){#S:after{content:" x 2559";}} @media (min-device-height:2560px){#S:after{content:" x 2560";}} @media (min-device-height:2561px){#S:after{content:"";}} ================================================ FILE: css/window_size.css ================================================ @media (min-width:399px){#D:before{content:"";}} @media (min-width:400px){#D:before{content:"400";}} @media (min-width:401px){#D:before{content:"401";}} @media (min-width:402px){#D:before{content:"402";}} @media (min-width:403px){#D:before{content:"403";}} @media (min-width:404px){#D:before{content:"404";}} @media (min-width:405px){#D:before{content:"405";}} @media (min-width:406px){#D:before{content:"406";}} @media (min-width:407px){#D:before{content:"407";}} @media (min-width:408px){#D:before{content:"408";}} @media (min-width:409px){#D:before{content:"409";}} @media (min-width:410px){#D:before{content:"410";}} @media (min-width:411px){#D:before{content:"411";}} @media (min-width:412px){#D:before{content:"412";}} @media (min-width:413px){#D:before{content:"413";}} @media (min-width:414px){#D:before{content:"414";}} @media (min-width:415px){#D:before{content:"415";}} @media (min-width:416px){#D:before{content:"416";}} @media (min-width:417px){#D:before{content:"417";}} @media (min-width:418px){#D:before{content:"418";}} @media (min-width:419px){#D:before{content:"419";}} @media (min-width:420px){#D:before{content:"420";}} @media (min-width:421px){#D:before{content:"421";}} @media (min-width:422px){#D:before{content:"422";}} @media (min-width:423px){#D:before{content:"423";}} @media (min-width:424px){#D:before{content:"424";}} @media (min-width:425px){#D:before{content:"425";}} @media (min-width:426px){#D:before{content:"426";}} @media (min-width:427px){#D:before{content:"427";}} @media (min-width:428px){#D:before{content:"428";}} @media (min-width:429px){#D:before{content:"429";}} @media (min-width:430px){#D:before{content:"430";}} @media (min-width:431px){#D:before{content:"431";}} @media (min-width:432px){#D:before{content:"432";}} @media (min-width:433px){#D:before{content:"433";}} @media (min-width:434px){#D:before{content:"434";}} @media (min-width:435px){#D:before{content:"435";}} @media (min-width:436px){#D:before{content:"436";}} @media (min-width:437px){#D:before{content:"437";}} @media (min-width:438px){#D:before{content:"438";}} @media (min-width:439px){#D:before{content:"439";}} @media (min-width:440px){#D:before{content:"440";}} @media (min-width:441px){#D:before{content:"441";}} @media (min-width:442px){#D:before{content:"442";}} @media (min-width:443px){#D:before{content:"443";}} @media (min-width:444px){#D:before{content:"444";}} @media (min-width:445px){#D:before{content:"445";}} @media (min-width:446px){#D:before{content:"446";}} @media (min-width:447px){#D:before{content:"447";}} @media (min-width:448px){#D:before{content:"448";}} @media (min-width:449px){#D:before{content:"449";}} @media (min-width:450px){#D:before{content:"450";}} @media (min-width:451px){#D:before{content:"451";}} @media (min-width:452px){#D:before{content:"452";}} @media (min-width:453px){#D:before{content:"453";}} @media (min-width:454px){#D:before{content:"454";}} @media (min-width:455px){#D:before{content:"455";}} @media (min-width:456px){#D:before{content:"456";}} @media (min-width:457px){#D:before{content:"457";}} @media (min-width:458px){#D:before{content:"458";}} @media (min-width:459px){#D:before{content:"459";}} @media (min-width:460px){#D:before{content:"460";}} @media (min-width:461px){#D:before{content:"461";}} @media (min-width:462px){#D:before{content:"462";}} @media (min-width:463px){#D:before{content:"463";}} @media (min-width:464px){#D:before{content:"464";}} @media (min-width:465px){#D:before{content:"465";}} @media (min-width:466px){#D:before{content:"466";}} @media (min-width:467px){#D:before{content:"467";}} @media (min-width:468px){#D:before{content:"468";}} @media (min-width:469px){#D:before{content:"469";}} @media (min-width:470px){#D:before{content:"470";}} @media (min-width:471px){#D:before{content:"471";}} @media (min-width:472px){#D:before{content:"472";}} @media (min-width:473px){#D:before{content:"473";}} @media (min-width:474px){#D:before{content:"474";}} @media (min-width:475px){#D:before{content:"475";}} @media (min-width:476px){#D:before{content:"476";}} @media (min-width:477px){#D:before{content:"477";}} @media (min-width:478px){#D:before{content:"478";}} @media (min-width:479px){#D:before{content:"479";}} @media (min-width:480px){#D:before{content:"480";}} @media (min-width:481px){#D:before{content:"481";}} @media (min-width:482px){#D:before{content:"482";}} @media (min-width:483px){#D:before{content:"483";}} @media (min-width:484px){#D:before{content:"484";}} @media (min-width:485px){#D:before{content:"485";}} @media (min-width:486px){#D:before{content:"486";}} @media (min-width:487px){#D:before{content:"487";}} @media (min-width:488px){#D:before{content:"488";}} @media (min-width:489px){#D:before{content:"489";}} @media (min-width:490px){#D:before{content:"490";}} @media (min-width:491px){#D:before{content:"491";}} @media (min-width:492px){#D:before{content:"492";}} @media (min-width:493px){#D:before{content:"493";}} @media (min-width:494px){#D:before{content:"494";}} @media (min-width:495px){#D:before{content:"495";}} @media (min-width:496px){#D:before{content:"496";}} @media (min-width:497px){#D:before{content:"497";}} @media (min-width:498px){#D:before{content:"498";}} @media (min-width:499px){#D:before{content:"499";}} @media (min-width:500px){#D:before{content:"500";}} @media (min-width:501px){#D:before{content:"501";}} @media (min-width:502px){#D:before{content:"502";}} @media (min-width:503px){#D:before{content:"503";}} @media (min-width:504px){#D:before{content:"504";}} @media (min-width:505px){#D:before{content:"505";}} @media (min-width:506px){#D:before{content:"506";}} @media (min-width:507px){#D:before{content:"507";}} @media (min-width:508px){#D:before{content:"508";}} @media (min-width:509px){#D:before{content:"509";}} @media (min-width:510px){#D:before{content:"510";}} @media (min-width:511px){#D:before{content:"511";}} @media (min-width:512px){#D:before{content:"512";}} @media (min-width:513px){#D:before{content:"513";}} @media (min-width:514px){#D:before{content:"514";}} @media (min-width:515px){#D:before{content:"515";}} @media (min-width:516px){#D:before{content:"516";}} @media (min-width:517px){#D:before{content:"517";}} @media (min-width:518px){#D:before{content:"518";}} @media (min-width:519px){#D:before{content:"519";}} @media (min-width:520px){#D:before{content:"520";}} @media (min-width:521px){#D:before{content:"521";}} @media (min-width:522px){#D:before{content:"522";}} @media (min-width:523px){#D:before{content:"523";}} @media (min-width:524px){#D:before{content:"524";}} @media (min-width:525px){#D:before{content:"525";}} @media (min-width:526px){#D:before{content:"526";}} @media (min-width:527px){#D:before{content:"527";}} @media (min-width:528px){#D:before{content:"528";}} @media (min-width:529px){#D:before{content:"529";}} @media (min-width:530px){#D:before{content:"530";}} @media (min-width:531px){#D:before{content:"531";}} @media (min-width:532px){#D:before{content:"532";}} @media (min-width:533px){#D:before{content:"533";}} @media (min-width:534px){#D:before{content:"534";}} @media (min-width:535px){#D:before{content:"535";}} @media (min-width:536px){#D:before{content:"536";}} @media (min-width:537px){#D:before{content:"537";}} @media (min-width:538px){#D:before{content:"538";}} @media (min-width:539px){#D:before{content:"539";}} @media (min-width:540px){#D:before{content:"540";}} @media (min-width:541px){#D:before{content:"541";}} @media (min-width:542px){#D:before{content:"542";}} @media (min-width:543px){#D:before{content:"543";}} @media (min-width:544px){#D:before{content:"544";}} @media (min-width:545px){#D:before{content:"545";}} @media (min-width:546px){#D:before{content:"546";}} @media (min-width:547px){#D:before{content:"547";}} @media (min-width:548px){#D:before{content:"548";}} @media (min-width:549px){#D:before{content:"549";}} @media (min-width:550px){#D:before{content:"550";}} @media (min-width:551px){#D:before{content:"551";}} @media (min-width:552px){#D:before{content:"552";}} @media (min-width:553px){#D:before{content:"553";}} @media (min-width:554px){#D:before{content:"554";}} @media (min-width:555px){#D:before{content:"555";}} @media (min-width:556px){#D:before{content:"556";}} @media (min-width:557px){#D:before{content:"557";}} @media (min-width:558px){#D:before{content:"558";}} @media (min-width:559px){#D:before{content:"559";}} @media (min-width:560px){#D:before{content:"560";}} @media (min-width:561px){#D:before{content:"561";}} @media (min-width:562px){#D:before{content:"562";}} @media (min-width:563px){#D:before{content:"563";}} @media (min-width:564px){#D:before{content:"564";}} @media (min-width:565px){#D:before{content:"565";}} @media (min-width:566px){#D:before{content:"566";}} @media (min-width:567px){#D:before{content:"567";}} @media (min-width:568px){#D:before{content:"568";}} @media (min-width:569px){#D:before{content:"569";}} @media (min-width:570px){#D:before{content:"570";}} @media (min-width:571px){#D:before{content:"571";}} @media (min-width:572px){#D:before{content:"572";}} @media (min-width:573px){#D:before{content:"573";}} @media (min-width:574px){#D:before{content:"574";}} @media (min-width:575px){#D:before{content:"575";}} @media (min-width:576px){#D:before{content:"576";}} @media (min-width:577px){#D:before{content:"577";}} @media (min-width:578px){#D:before{content:"578";}} @media (min-width:579px){#D:before{content:"579";}} @media (min-width:580px){#D:before{content:"580";}} @media (min-width:581px){#D:before{content:"581";}} @media (min-width:582px){#D:before{content:"582";}} @media (min-width:583px){#D:before{content:"583";}} @media (min-width:584px){#D:before{content:"584";}} @media (min-width:585px){#D:before{content:"585";}} @media (min-width:586px){#D:before{content:"586";}} @media (min-width:587px){#D:before{content:"587";}} @media (min-width:588px){#D:before{content:"588";}} @media (min-width:589px){#D:before{content:"589";}} @media (min-width:590px){#D:before{content:"590";}} @media (min-width:591px){#D:before{content:"591";}} @media (min-width:592px){#D:before{content:"592";}} @media (min-width:593px){#D:before{content:"593";}} @media (min-width:594px){#D:before{content:"594";}} @media (min-width:595px){#D:before{content:"595";}} @media (min-width:596px){#D:before{content:"596";}} @media (min-width:597px){#D:before{content:"597";}} @media (min-width:598px){#D:before{content:"598";}} @media (min-width:599px){#D:before{content:"599";}} @media (min-width:600px){#D:before{content:"600";}} @media (min-width:601px){#D:before{content:"601";}} @media (min-width:602px){#D:before{content:"602";}} @media (min-width:603px){#D:before{content:"603";}} @media (min-width:604px){#D:before{content:"604";}} @media (min-width:605px){#D:before{content:"605";}} @media (min-width:606px){#D:before{content:"606";}} @media (min-width:607px){#D:before{content:"607";}} @media (min-width:608px){#D:before{content:"608";}} @media (min-width:609px){#D:before{content:"609";}} @media (min-width:610px){#D:before{content:"610";}} @media (min-width:611px){#D:before{content:"611";}} @media (min-width:612px){#D:before{content:"612";}} @media (min-width:613px){#D:before{content:"613";}} @media (min-width:614px){#D:before{content:"614";}} @media (min-width:615px){#D:before{content:"615";}} @media (min-width:616px){#D:before{content:"616";}} @media (min-width:617px){#D:before{content:"617";}} @media (min-width:618px){#D:before{content:"618";}} @media (min-width:619px){#D:before{content:"619";}} @media (min-width:620px){#D:before{content:"620";}} @media (min-width:621px){#D:before{content:"621";}} @media (min-width:622px){#D:before{content:"622";}} @media (min-width:623px){#D:before{content:"623";}} @media (min-width:624px){#D:before{content:"624";}} @media (min-width:625px){#D:before{content:"625";}} @media (min-width:626px){#D:before{content:"626";}} @media (min-width:627px){#D:before{content:"627";}} @media (min-width:628px){#D:before{content:"628";}} @media (min-width:629px){#D:before{content:"629";}} @media (min-width:630px){#D:before{content:"630";}} @media (min-width:631px){#D:before{content:"631";}} @media (min-width:632px){#D:before{content:"632";}} @media (min-width:633px){#D:before{content:"633";}} @media (min-width:634px){#D:before{content:"634";}} @media (min-width:635px){#D:before{content:"635";}} @media (min-width:636px){#D:before{content:"636";}} @media (min-width:637px){#D:before{content:"637";}} @media (min-width:638px){#D:before{content:"638";}} @media (min-width:639px){#D:before{content:"639";}} @media (min-width:640px){#D:before{content:"640";}} @media (min-width:641px){#D:before{content:"641";}} @media (min-width:642px){#D:before{content:"642";}} @media (min-width:643px){#D:before{content:"643";}} @media (min-width:644px){#D:before{content:"644";}} @media (min-width:645px){#D:before{content:"645";}} @media (min-width:646px){#D:before{content:"646";}} @media (min-width:647px){#D:before{content:"647";}} @media (min-width:648px){#D:before{content:"648";}} @media (min-width:649px){#D:before{content:"649";}} @media (min-width:650px){#D:before{content:"650";}} @media (min-width:651px){#D:before{content:"651";}} @media (min-width:652px){#D:before{content:"652";}} @media (min-width:653px){#D:before{content:"653";}} @media (min-width:654px){#D:before{content:"654";}} @media (min-width:655px){#D:before{content:"655";}} @media (min-width:656px){#D:before{content:"656";}} @media (min-width:657px){#D:before{content:"657";}} @media (min-width:658px){#D:before{content:"658";}} @media (min-width:659px){#D:before{content:"659";}} @media (min-width:660px){#D:before{content:"660";}} @media (min-width:661px){#D:before{content:"661";}} @media (min-width:662px){#D:before{content:"662";}} @media (min-width:663px){#D:before{content:"663";}} @media (min-width:664px){#D:before{content:"664";}} @media (min-width:665px){#D:before{content:"665";}} @media (min-width:666px){#D:before{content:"666";}} @media (min-width:667px){#D:before{content:"667";}} @media (min-width:668px){#D:before{content:"668";}} @media (min-width:669px){#D:before{content:"669";}} @media (min-width:670px){#D:before{content:"670";}} @media (min-width:671px){#D:before{content:"671";}} @media (min-width:672px){#D:before{content:"672";}} @media (min-width:673px){#D:before{content:"673";}} @media (min-width:674px){#D:before{content:"674";}} @media (min-width:675px){#D:before{content:"675";}} @media (min-width:676px){#D:before{content:"676";}} @media (min-width:677px){#D:before{content:"677";}} @media (min-width:678px){#D:before{content:"678";}} @media (min-width:679px){#D:before{content:"679";}} @media (min-width:680px){#D:before{content:"680";}} @media (min-width:681px){#D:before{content:"681";}} @media (min-width:682px){#D:before{content:"682";}} @media (min-width:683px){#D:before{content:"683";}} @media (min-width:684px){#D:before{content:"684";}} @media (min-width:685px){#D:before{content:"685";}} @media (min-width:686px){#D:before{content:"686";}} @media (min-width:687px){#D:before{content:"687";}} @media (min-width:688px){#D:before{content:"688";}} @media (min-width:689px){#D:before{content:"689";}} @media (min-width:690px){#D:before{content:"690";}} @media (min-width:691px){#D:before{content:"691";}} @media (min-width:692px){#D:before{content:"692";}} @media (min-width:693px){#D:before{content:"693";}} @media (min-width:694px){#D:before{content:"694";}} @media (min-width:695px){#D:before{content:"695";}} @media (min-width:696px){#D:before{content:"696";}} @media (min-width:697px){#D:before{content:"697";}} @media (min-width:698px){#D:before{content:"698";}} @media (min-width:699px){#D:before{content:"699";}} @media (min-width:700px){#D:before{content:"700";}} @media (min-width:701px){#D:before{content:"701";}} @media (min-width:702px){#D:before{content:"702";}} @media (min-width:703px){#D:before{content:"703";}} @media (min-width:704px){#D:before{content:"704";}} @media (min-width:705px){#D:before{content:"705";}} @media (min-width:706px){#D:before{content:"706";}} @media (min-width:707px){#D:before{content:"707";}} @media (min-width:708px){#D:before{content:"708";}} @media (min-width:709px){#D:before{content:"709";}} @media (min-width:710px){#D:before{content:"710";}} @media (min-width:711px){#D:before{content:"711";}} @media (min-width:712px){#D:before{content:"712";}} @media (min-width:713px){#D:before{content:"713";}} @media (min-width:714px){#D:before{content:"714";}} @media (min-width:715px){#D:before{content:"715";}} @media (min-width:716px){#D:before{content:"716";}} @media (min-width:717px){#D:before{content:"717";}} @media (min-width:718px){#D:before{content:"718";}} @media (min-width:719px){#D:before{content:"719";}} @media (min-width:720px){#D:before{content:"720";}} @media (min-width:721px){#D:before{content:"721";}} @media (min-width:722px){#D:before{content:"722";}} @media (min-width:723px){#D:before{content:"723";}} @media (min-width:724px){#D:before{content:"724";}} @media (min-width:725px){#D:before{content:"725";}} @media (min-width:726px){#D:before{content:"726";}} @media (min-width:727px){#D:before{content:"727";}} @media (min-width:728px){#D:before{content:"728";}} @media (min-width:729px){#D:before{content:"729";}} @media (min-width:730px){#D:before{content:"730";}} @media (min-width:731px){#D:before{content:"731";}} @media (min-width:732px){#D:before{content:"732";}} @media (min-width:733px){#D:before{content:"733";}} @media (min-width:734px){#D:before{content:"734";}} @media (min-width:735px){#D:before{content:"735";}} @media (min-width:736px){#D:before{content:"736";}} @media (min-width:737px){#D:before{content:"737";}} @media (min-width:738px){#D:before{content:"738";}} @media (min-width:739px){#D:before{content:"739";}} @media (min-width:740px){#D:before{content:"740";}} @media (min-width:741px){#D:before{content:"741";}} @media (min-width:742px){#D:before{content:"742";}} @media (min-width:743px){#D:before{content:"743";}} @media (min-width:744px){#D:before{content:"744";}} @media (min-width:745px){#D:before{content:"745";}} @media (min-width:746px){#D:before{content:"746";}} @media (min-width:747px){#D:before{content:"747";}} @media (min-width:748px){#D:before{content:"748";}} @media (min-width:749px){#D:before{content:"749";}} @media (min-width:750px){#D:before{content:"750";}} @media (min-width:751px){#D:before{content:"751";}} @media (min-width:752px){#D:before{content:"752";}} @media (min-width:753px){#D:before{content:"753";}} @media (min-width:754px){#D:before{content:"754";}} @media (min-width:755px){#D:before{content:"755";}} @media (min-width:756px){#D:before{content:"756";}} @media (min-width:757px){#D:before{content:"757";}} @media (min-width:758px){#D:before{content:"758";}} @media (min-width:759px){#D:before{content:"759";}} @media (min-width:760px){#D:before{content:"760";}} @media (min-width:761px){#D:before{content:"761";}} @media (min-width:762px){#D:before{content:"762";}} @media (min-width:763px){#D:before{content:"763";}} @media (min-width:764px){#D:before{content:"764";}} @media (min-width:765px){#D:before{content:"765";}} @media (min-width:766px){#D:before{content:"766";}} @media (min-width:767px){#D:before{content:"767";}} @media (min-width:768px){#D:before{content:"768";}} @media (min-width:769px){#D:before{content:"769";}} @media (min-width:770px){#D:before{content:"770";}} @media (min-width:771px){#D:before{content:"771";}} @media (min-width:772px){#D:before{content:"772";}} @media (min-width:773px){#D:before{content:"773";}} @media (min-width:774px){#D:before{content:"774";}} @media (min-width:775px){#D:before{content:"775";}} @media (min-width:776px){#D:before{content:"776";}} @media (min-width:777px){#D:before{content:"777";}} @media (min-width:778px){#D:before{content:"778";}} @media (min-width:779px){#D:before{content:"779";}} @media (min-width:780px){#D:before{content:"780";}} @media (min-width:781px){#D:before{content:"781";}} @media (min-width:782px){#D:before{content:"782";}} @media (min-width:783px){#D:before{content:"783";}} @media (min-width:784px){#D:before{content:"784";}} @media (min-width:785px){#D:before{content:"785";}} @media (min-width:786px){#D:before{content:"786";}} @media (min-width:787px){#D:before{content:"787";}} @media (min-width:788px){#D:before{content:"788";}} @media (min-width:789px){#D:before{content:"789";}} @media (min-width:790px){#D:before{content:"790";}} @media (min-width:791px){#D:before{content:"791";}} @media (min-width:792px){#D:before{content:"792";}} @media (min-width:793px){#D:before{content:"793";}} @media (min-width:794px){#D:before{content:"794";}} @media (min-width:795px){#D:before{content:"795";}} @media (min-width:796px){#D:before{content:"796";}} @media (min-width:797px){#D:before{content:"797";}} @media (min-width:798px){#D:before{content:"798";}} @media (min-width:799px){#D:before{content:"799";}} @media (min-width:800px){#D:before{content:"800";}} @media (min-width:801px){#D:before{content:"801";}} @media (min-width:802px){#D:before{content:"802";}} @media (min-width:803px){#D:before{content:"803";}} @media (min-width:804px){#D:before{content:"804";}} @media (min-width:805px){#D:before{content:"805";}} @media (min-width:806px){#D:before{content:"806";}} @media (min-width:807px){#D:before{content:"807";}} @media (min-width:808px){#D:before{content:"808";}} @media (min-width:809px){#D:before{content:"809";}} @media (min-width:810px){#D:before{content:"810";}} @media (min-width:811px){#D:before{content:"811";}} @media (min-width:812px){#D:before{content:"812";}} @media (min-width:813px){#D:before{content:"813";}} @media (min-width:814px){#D:before{content:"814";}} @media (min-width:815px){#D:before{content:"815";}} @media (min-width:816px){#D:before{content:"816";}} @media (min-width:817px){#D:before{content:"817";}} @media (min-width:818px){#D:before{content:"818";}} @media (min-width:819px){#D:before{content:"819";}} @media (min-width:820px){#D:before{content:"820";}} @media (min-width:821px){#D:before{content:"821";}} @media (min-width:822px){#D:before{content:"822";}} @media (min-width:823px){#D:before{content:"823";}} @media (min-width:824px){#D:before{content:"824";}} @media (min-width:825px){#D:before{content:"825";}} @media (min-width:826px){#D:before{content:"826";}} @media (min-width:827px){#D:before{content:"827";}} @media (min-width:828px){#D:before{content:"828";}} @media (min-width:829px){#D:before{content:"829";}} @media (min-width:830px){#D:before{content:"830";}} @media (min-width:831px){#D:before{content:"831";}} @media (min-width:832px){#D:before{content:"832";}} @media (min-width:833px){#D:before{content:"833";}} @media (min-width:834px){#D:before{content:"834";}} @media (min-width:835px){#D:before{content:"835";}} @media (min-width:836px){#D:before{content:"836";}} @media (min-width:837px){#D:before{content:"837";}} @media (min-width:838px){#D:before{content:"838";}} @media (min-width:839px){#D:before{content:"839";}} @media (min-width:840px){#D:before{content:"840";}} @media (min-width:841px){#D:before{content:"841";}} @media (min-width:842px){#D:before{content:"842";}} @media (min-width:843px){#D:before{content:"843";}} @media (min-width:844px){#D:before{content:"844";}} @media (min-width:845px){#D:before{content:"845";}} @media (min-width:846px){#D:before{content:"846";}} @media (min-width:847px){#D:before{content:"847";}} @media (min-width:848px){#D:before{content:"848";}} @media (min-width:849px){#D:before{content:"849";}} @media (min-width:850px){#D:before{content:"850";}} @media (min-width:851px){#D:before{content:"851";}} @media (min-width:852px){#D:before{content:"852";}} @media (min-width:853px){#D:before{content:"853";}} @media (min-width:854px){#D:before{content:"854";}} @media (min-width:855px){#D:before{content:"855";}} @media (min-width:856px){#D:before{content:"856";}} @media (min-width:857px){#D:before{content:"857";}} @media (min-width:858px){#D:before{content:"858";}} @media (min-width:859px){#D:before{content:"859";}} @media (min-width:860px){#D:before{content:"860";}} @media (min-width:861px){#D:before{content:"861";}} @media (min-width:862px){#D:before{content:"862";}} @media (min-width:863px){#D:before{content:"863";}} @media (min-width:864px){#D:before{content:"864";}} @media (min-width:865px){#D:before{content:"865";}} @media (min-width:866px){#D:before{content:"866";}} @media (min-width:867px){#D:before{content:"867";}} @media (min-width:868px){#D:before{content:"868";}} @media (min-width:869px){#D:before{content:"869";}} @media (min-width:870px){#D:before{content:"870";}} @media (min-width:871px){#D:before{content:"871";}} @media (min-width:872px){#D:before{content:"872";}} @media (min-width:873px){#D:before{content:"873";}} @media (min-width:874px){#D:before{content:"874";}} @media (min-width:875px){#D:before{content:"875";}} @media (min-width:876px){#D:before{content:"876";}} @media (min-width:877px){#D:before{content:"877";}} @media (min-width:878px){#D:before{content:"878";}} @media (min-width:879px){#D:before{content:"879";}} @media (min-width:880px){#D:before{content:"880";}} @media (min-width:881px){#D:before{content:"881";}} @media (min-width:882px){#D:before{content:"882";}} @media (min-width:883px){#D:before{content:"883";}} @media (min-width:884px){#D:before{content:"884";}} @media (min-width:885px){#D:before{content:"885";}} @media (min-width:886px){#D:before{content:"886";}} @media (min-width:887px){#D:before{content:"887";}} @media (min-width:888px){#D:before{content:"888";}} @media (min-width:889px){#D:before{content:"889";}} @media (min-width:890px){#D:before{content:"890";}} @media (min-width:891px){#D:before{content:"891";}} @media (min-width:892px){#D:before{content:"892";}} @media (min-width:893px){#D:before{content:"893";}} @media (min-width:894px){#D:before{content:"894";}} @media (min-width:895px){#D:before{content:"895";}} @media (min-width:896px){#D:before{content:"896";}} @media (min-width:897px){#D:before{content:"897";}} @media (min-width:898px){#D:before{content:"898";}} @media (min-width:899px){#D:before{content:"899";}} @media (min-width:900px){#D:before{content:"900";}} @media (min-width:901px){#D:before{content:"901";}} @media (min-width:902px){#D:before{content:"902";}} @media (min-width:903px){#D:before{content:"903";}} @media (min-width:904px){#D:before{content:"904";}} @media (min-width:905px){#D:before{content:"905";}} @media (min-width:906px){#D:before{content:"906";}} @media (min-width:907px){#D:before{content:"907";}} @media (min-width:908px){#D:before{content:"908";}} @media (min-width:909px){#D:before{content:"909";}} @media (min-width:910px){#D:before{content:"910";}} @media (min-width:911px){#D:before{content:"911";}} @media (min-width:912px){#D:before{content:"912";}} @media (min-width:913px){#D:before{content:"913";}} @media (min-width:914px){#D:before{content:"914";}} @media (min-width:915px){#D:before{content:"915";}} @media (min-width:916px){#D:before{content:"916";}} @media (min-width:917px){#D:before{content:"917";}} @media (min-width:918px){#D:before{content:"918";}} @media (min-width:919px){#D:before{content:"919";}} @media (min-width:920px){#D:before{content:"920";}} @media (min-width:921px){#D:before{content:"921";}} @media (min-width:922px){#D:before{content:"922";}} @media (min-width:923px){#D:before{content:"923";}} @media (min-width:924px){#D:before{content:"924";}} @media (min-width:925px){#D:before{content:"925";}} @media (min-width:926px){#D:before{content:"926";}} @media (min-width:927px){#D:before{content:"927";}} @media (min-width:928px){#D:before{content:"928";}} @media (min-width:929px){#D:before{content:"929";}} @media (min-width:930px){#D:before{content:"930";}} @media (min-width:931px){#D:before{content:"931";}} @media (min-width:932px){#D:before{content:"932";}} @media (min-width:933px){#D:before{content:"933";}} @media (min-width:934px){#D:before{content:"934";}} @media (min-width:935px){#D:before{content:"935";}} @media (min-width:936px){#D:before{content:"936";}} @media (min-width:937px){#D:before{content:"937";}} @media (min-width:938px){#D:before{content:"938";}} @media (min-width:939px){#D:before{content:"939";}} @media (min-width:940px){#D:before{content:"940";}} @media (min-width:941px){#D:before{content:"941";}} @media (min-width:942px){#D:before{content:"942";}} @media (min-width:943px){#D:before{content:"943";}} @media (min-width:944px){#D:before{content:"944";}} @media (min-width:945px){#D:before{content:"945";}} @media (min-width:946px){#D:before{content:"946";}} @media (min-width:947px){#D:before{content:"947";}} @media (min-width:948px){#D:before{content:"948";}} @media (min-width:949px){#D:before{content:"949";}} @media (min-width:950px){#D:before{content:"950";}} @media (min-width:951px){#D:before{content:"951";}} @media (min-width:952px){#D:before{content:"952";}} @media (min-width:953px){#D:before{content:"953";}} @media (min-width:954px){#D:before{content:"954";}} @media (min-width:955px){#D:before{content:"955";}} @media (min-width:956px){#D:before{content:"956";}} @media (min-width:957px){#D:before{content:"957";}} @media (min-width:958px){#D:before{content:"958";}} @media (min-width:959px){#D:before{content:"959";}} @media (min-width:960px){#D:before{content:"960";}} @media (min-width:961px){#D:before{content:"961";}} @media (min-width:962px){#D:before{content:"962";}} @media (min-width:963px){#D:before{content:"963";}} @media (min-width:964px){#D:before{content:"964";}} @media (min-width:965px){#D:before{content:"965";}} @media (min-width:966px){#D:before{content:"966";}} @media (min-width:967px){#D:before{content:"967";}} @media (min-width:968px){#D:before{content:"968";}} @media (min-width:969px){#D:before{content:"969";}} @media (min-width:970px){#D:before{content:"970";}} @media (min-width:971px){#D:before{content:"971";}} @media (min-width:972px){#D:before{content:"972";}} @media (min-width:973px){#D:before{content:"973";}} @media (min-width:974px){#D:before{content:"974";}} @media (min-width:975px){#D:before{content:"975";}} @media (min-width:976px){#D:before{content:"976";}} @media (min-width:977px){#D:before{content:"977";}} @media (min-width:978px){#D:before{content:"978";}} @media (min-width:979px){#D:before{content:"979";}} @media (min-width:980px){#D:before{content:"980";}} @media (min-width:981px){#D:before{content:"981";}} @media (min-width:982px){#D:before{content:"982";}} @media (min-width:983px){#D:before{content:"983";}} @media (min-width:984px){#D:before{content:"984";}} @media (min-width:985px){#D:before{content:"985";}} @media (min-width:986px){#D:before{content:"986";}} @media (min-width:987px){#D:before{content:"987";}} @media (min-width:988px){#D:before{content:"988";}} @media (min-width:989px){#D:before{content:"989";}} @media (min-width:990px){#D:before{content:"990";}} @media (min-width:991px){#D:before{content:"991";}} @media (min-width:992px){#D:before{content:"992";}} @media (min-width:993px){#D:before{content:"993";}} @media (min-width:994px){#D:before{content:"994";}} @media (min-width:995px){#D:before{content:"995";}} @media (min-width:996px){#D:before{content:"996";}} @media (min-width:997px){#D:before{content:"997";}} @media (min-width:998px){#D:before{content:"998";}} @media (min-width:999px){#D:before{content:"999";}} @media (min-width:1000px){#D:before{content:"1000";}} @media (min-width:1001px){#D:before{content:"1001";}} @media (min-width:1002px){#D:before{content:"1002";}} @media (min-width:1003px){#D:before{content:"1003";}} @media (min-width:1004px){#D:before{content:"1004";}} @media (min-width:1005px){#D:before{content:"1005";}} @media (min-width:1006px){#D:before{content:"1006";}} @media (min-width:1007px){#D:before{content:"1007";}} @media (min-width:1008px){#D:before{content:"1008";}} @media (min-width:1009px){#D:before{content:"1009";}} @media (min-width:1010px){#D:before{content:"1010";}} @media (min-width:1011px){#D:before{content:"1011";}} @media (min-width:1012px){#D:before{content:"1012";}} @media (min-width:1013px){#D:before{content:"1013";}} @media (min-width:1014px){#D:before{content:"1014";}} @media (min-width:1015px){#D:before{content:"1015";}} @media (min-width:1016px){#D:before{content:"1016";}} @media (min-width:1017px){#D:before{content:"1017";}} @media (min-width:1018px){#D:before{content:"1018";}} @media (min-width:1019px){#D:before{content:"1019";}} @media (min-width:1020px){#D:before{content:"1020";}} @media (min-width:1021px){#D:before{content:"1021";}} @media (min-width:1022px){#D:before{content:"1022";}} @media (min-width:1023px){#D:before{content:"1023";}} @media (min-width:1024px){#D:before{content:"1024";}} @media (min-width:1025px){#D:before{content:"1025";}} @media (min-width:1026px){#D:before{content:"1026";}} @media (min-width:1027px){#D:before{content:"1027";}} @media (min-width:1028px){#D:before{content:"1028";}} @media (min-width:1029px){#D:before{content:"1029";}} @media (min-width:1030px){#D:before{content:"1030";}} @media (min-width:1031px){#D:before{content:"1031";}} @media (min-width:1032px){#D:before{content:"1032";}} @media (min-width:1033px){#D:before{content:"1033";}} @media (min-width:1034px){#D:before{content:"1034";}} @media (min-width:1035px){#D:before{content:"1035";}} @media (min-width:1036px){#D:before{content:"1036";}} @media (min-width:1037px){#D:before{content:"1037";}} @media (min-width:1038px){#D:before{content:"1038";}} @media (min-width:1039px){#D:before{content:"1039";}} @media (min-width:1040px){#D:before{content:"1040";}} @media (min-width:1041px){#D:before{content:"1041";}} @media (min-width:1042px){#D:before{content:"1042";}} @media (min-width:1043px){#D:before{content:"1043";}} @media (min-width:1044px){#D:before{content:"1044";}} @media (min-width:1045px){#D:before{content:"1045";}} @media (min-width:1046px){#D:before{content:"1046";}} @media (min-width:1047px){#D:before{content:"1047";}} @media (min-width:1048px){#D:before{content:"1048";}} @media (min-width:1049px){#D:before{content:"1049";}} @media (min-width:1050px){#D:before{content:"1050";}} @media (min-width:1051px){#D:before{content:"1051";}} @media (min-width:1052px){#D:before{content:"1052";}} @media (min-width:1053px){#D:before{content:"1053";}} @media (min-width:1054px){#D:before{content:"1054";}} @media (min-width:1055px){#D:before{content:"1055";}} @media (min-width:1056px){#D:before{content:"1056";}} @media (min-width:1057px){#D:before{content:"1057";}} @media (min-width:1058px){#D:before{content:"1058";}} @media (min-width:1059px){#D:before{content:"1059";}} @media (min-width:1060px){#D:before{content:"1060";}} @media (min-width:1061px){#D:before{content:"1061";}} @media (min-width:1062px){#D:before{content:"1062";}} @media (min-width:1063px){#D:before{content:"1063";}} @media (min-width:1064px){#D:before{content:"1064";}} @media (min-width:1065px){#D:before{content:"1065";}} @media (min-width:1066px){#D:before{content:"1066";}} @media (min-width:1067px){#D:before{content:"1067";}} @media (min-width:1068px){#D:before{content:"1068";}} @media (min-width:1069px){#D:before{content:"1069";}} @media (min-width:1070px){#D:before{content:"1070";}} @media (min-width:1071px){#D:before{content:"1071";}} @media (min-width:1072px){#D:before{content:"1072";}} @media (min-width:1073px){#D:before{content:"1073";}} @media (min-width:1074px){#D:before{content:"1074";}} @media (min-width:1075px){#D:before{content:"1075";}} @media (min-width:1076px){#D:before{content:"1076";}} @media (min-width:1077px){#D:before{content:"1077";}} @media (min-width:1078px){#D:before{content:"1078";}} @media (min-width:1079px){#D:before{content:"1079";}} @media (min-width:1080px){#D:before{content:"1080";}} @media (min-width:1081px){#D:before{content:"1081";}} @media (min-width:1082px){#D:before{content:"1082";}} @media (min-width:1083px){#D:before{content:"1083";}} @media (min-width:1084px){#D:before{content:"1084";}} @media (min-width:1085px){#D:before{content:"1085";}} @media (min-width:1086px){#D:before{content:"1086";}} @media (min-width:1087px){#D:before{content:"1087";}} @media (min-width:1088px){#D:before{content:"1088";}} @media (min-width:1089px){#D:before{content:"1089";}} @media (min-width:1090px){#D:before{content:"1090";}} @media (min-width:1091px){#D:before{content:"1091";}} @media (min-width:1092px){#D:before{content:"1092";}} @media (min-width:1093px){#D:before{content:"1093";}} @media (min-width:1094px){#D:before{content:"1094";}} @media (min-width:1095px){#D:before{content:"1095";}} @media (min-width:1096px){#D:before{content:"1096";}} @media (min-width:1097px){#D:before{content:"1097";}} @media (min-width:1098px){#D:before{content:"1098";}} @media (min-width:1099px){#D:before{content:"1099";}} @media (min-width:1100px){#D:before{content:"1100";}} @media (min-width:1101px){#D:before{content:"1101";}} @media (min-width:1102px){#D:before{content:"1102";}} @media (min-width:1103px){#D:before{content:"1103";}} @media (min-width:1104px){#D:before{content:"1104";}} @media (min-width:1105px){#D:before{content:"1105";}} @media (min-width:1106px){#D:before{content:"1106";}} @media (min-width:1107px){#D:before{content:"1107";}} @media (min-width:1108px){#D:before{content:"1108";}} @media (min-width:1109px){#D:before{content:"1109";}} @media (min-width:1110px){#D:before{content:"1110";}} @media (min-width:1111px){#D:before{content:"1111";}} @media (min-width:1112px){#D:before{content:"1112";}} @media (min-width:1113px){#D:before{content:"1113";}} @media (min-width:1114px){#D:before{content:"1114";}} @media (min-width:1115px){#D:before{content:"1115";}} @media (min-width:1116px){#D:before{content:"1116";}} @media (min-width:1117px){#D:before{content:"1117";}} @media (min-width:1118px){#D:before{content:"1118";}} @media (min-width:1119px){#D:before{content:"1119";}} @media (min-width:1120px){#D:before{content:"1120";}} @media (min-width:1121px){#D:before{content:"1121";}} @media (min-width:1122px){#D:before{content:"1122";}} @media (min-width:1123px){#D:before{content:"1123";}} @media (min-width:1124px){#D:before{content:"1124";}} @media (min-width:1125px){#D:before{content:"1125";}} @media (min-width:1126px){#D:before{content:"1126";}} @media (min-width:1127px){#D:before{content:"1127";}} @media (min-width:1128px){#D:before{content:"1128";}} @media (min-width:1129px){#D:before{content:"1129";}} @media (min-width:1130px){#D:before{content:"1130";}} @media (min-width:1131px){#D:before{content:"1131";}} @media (min-width:1132px){#D:before{content:"1132";}} @media (min-width:1133px){#D:before{content:"1133";}} @media (min-width:1134px){#D:before{content:"1134";}} @media (min-width:1135px){#D:before{content:"1135";}} @media (min-width:1136px){#D:before{content:"1136";}} @media (min-width:1137px){#D:before{content:"1137";}} @media (min-width:1138px){#D:before{content:"1138";}} @media (min-width:1139px){#D:before{content:"1139";}} @media (min-width:1140px){#D:before{content:"1140";}} @media (min-width:1141px){#D:before{content:"1141";}} @media (min-width:1142px){#D:before{content:"1142";}} @media (min-width:1143px){#D:before{content:"1143";}} @media (min-width:1144px){#D:before{content:"1144";}} @media (min-width:1145px){#D:before{content:"1145";}} @media (min-width:1146px){#D:before{content:"1146";}} @media (min-width:1147px){#D:before{content:"1147";}} @media (min-width:1148px){#D:before{content:"1148";}} @media (min-width:1149px){#D:before{content:"1149";}} @media (min-width:1150px){#D:before{content:"1150";}} @media (min-width:1151px){#D:before{content:"1151";}} @media (min-width:1152px){#D:before{content:"1152";}} @media (min-width:1153px){#D:before{content:"1153";}} @media (min-width:1154px){#D:before{content:"1154";}} @media (min-width:1155px){#D:before{content:"1155";}} @media (min-width:1156px){#D:before{content:"1156";}} @media (min-width:1157px){#D:before{content:"1157";}} @media (min-width:1158px){#D:before{content:"1158";}} @media (min-width:1159px){#D:before{content:"1159";}} @media (min-width:1160px){#D:before{content:"1160";}} @media (min-width:1161px){#D:before{content:"1161";}} @media (min-width:1162px){#D:before{content:"1162";}} @media (min-width:1163px){#D:before{content:"1163";}} @media (min-width:1164px){#D:before{content:"1164";}} @media (min-width:1165px){#D:before{content:"1165";}} @media (min-width:1166px){#D:before{content:"1166";}} @media (min-width:1167px){#D:before{content:"1167";}} @media (min-width:1168px){#D:before{content:"1168";}} @media (min-width:1169px){#D:before{content:"1169";}} @media (min-width:1170px){#D:before{content:"1170";}} @media (min-width:1171px){#D:before{content:"1171";}} @media (min-width:1172px){#D:before{content:"1172";}} @media (min-width:1173px){#D:before{content:"1173";}} @media (min-width:1174px){#D:before{content:"1174";}} @media (min-width:1175px){#D:before{content:"1175";}} @media (min-width:1176px){#D:before{content:"1176";}} @media (min-width:1177px){#D:before{content:"1177";}} @media (min-width:1178px){#D:before{content:"1178";}} @media (min-width:1179px){#D:before{content:"1179";}} @media (min-width:1180px){#D:before{content:"1180";}} @media (min-width:1181px){#D:before{content:"1181";}} @media (min-width:1182px){#D:before{content:"1182";}} @media (min-width:1183px){#D:before{content:"1183";}} @media (min-width:1184px){#D:before{content:"1184";}} @media (min-width:1185px){#D:before{content:"1185";}} @media (min-width:1186px){#D:before{content:"1186";}} @media (min-width:1187px){#D:before{content:"1187";}} @media (min-width:1188px){#D:before{content:"1188";}} @media (min-width:1189px){#D:before{content:"1189";}} @media (min-width:1190px){#D:before{content:"1190";}} @media (min-width:1191px){#D:before{content:"1191";}} @media (min-width:1192px){#D:before{content:"1192";}} @media (min-width:1193px){#D:before{content:"1193";}} @media (min-width:1194px){#D:before{content:"1194";}} @media (min-width:1195px){#D:before{content:"1195";}} @media (min-width:1196px){#D:before{content:"1196";}} @media (min-width:1197px){#D:before{content:"1197";}} @media (min-width:1198px){#D:before{content:"1198";}} @media (min-width:1199px){#D:before{content:"1199";}} @media (min-width:1200px){#D:before{content:"1200";}} @media (min-width:1201px){#D:before{content:"1201";}} @media (min-width:1202px){#D:before{content:"1202";}} @media (min-width:1203px){#D:before{content:"1203";}} @media (min-width:1204px){#D:before{content:"1204";}} @media (min-width:1205px){#D:before{content:"1205";}} @media (min-width:1206px){#D:before{content:"1206";}} @media (min-width:1207px){#D:before{content:"1207";}} @media (min-width:1208px){#D:before{content:"1208";}} @media (min-width:1209px){#D:before{content:"1209";}} @media (min-width:1210px){#D:before{content:"1210";}} @media (min-width:1211px){#D:before{content:"1211";}} @media (min-width:1212px){#D:before{content:"1212";}} @media (min-width:1213px){#D:before{content:"1213";}} @media (min-width:1214px){#D:before{content:"1214";}} @media (min-width:1215px){#D:before{content:"1215";}} @media (min-width:1216px){#D:before{content:"1216";}} @media (min-width:1217px){#D:before{content:"1217";}} @media (min-width:1218px){#D:before{content:"1218";}} @media (min-width:1219px){#D:before{content:"1219";}} @media (min-width:1220px){#D:before{content:"1220";}} @media (min-width:1221px){#D:before{content:"1221";}} @media (min-width:1222px){#D:before{content:"1222";}} @media (min-width:1223px){#D:before{content:"1223";}} @media (min-width:1224px){#D:before{content:"1224";}} @media (min-width:1225px){#D:before{content:"1225";}} @media (min-width:1226px){#D:before{content:"1226";}} @media (min-width:1227px){#D:before{content:"1227";}} @media (min-width:1228px){#D:before{content:"1228";}} @media (min-width:1229px){#D:before{content:"1229";}} @media (min-width:1230px){#D:before{content:"1230";}} @media (min-width:1231px){#D:before{content:"1231";}} @media (min-width:1232px){#D:before{content:"1232";}} @media (min-width:1233px){#D:before{content:"1233";}} @media (min-width:1234px){#D:before{content:"1234";}} @media (min-width:1235px){#D:before{content:"1235";}} @media (min-width:1236px){#D:before{content:"1236";}} @media (min-width:1237px){#D:before{content:"1237";}} @media (min-width:1238px){#D:before{content:"1238";}} @media (min-width:1239px){#D:before{content:"1239";}} @media (min-width:1240px){#D:before{content:"1240";}} @media (min-width:1241px){#D:before{content:"1241";}} @media (min-width:1242px){#D:before{content:"1242";}} @media (min-width:1243px){#D:before{content:"1243";}} @media (min-width:1244px){#D:before{content:"1244";}} @media (min-width:1245px){#D:before{content:"1245";}} @media (min-width:1246px){#D:before{content:"1246";}} @media (min-width:1247px){#D:before{content:"1247";}} @media (min-width:1248px){#D:before{content:"1248";}} @media (min-width:1249px){#D:before{content:"1249";}} @media (min-width:1250px){#D:before{content:"1250";}} @media (min-width:1251px){#D:before{content:"1251";}} @media (min-width:1252px){#D:before{content:"1252";}} @media (min-width:1253px){#D:before{content:"1253";}} @media (min-width:1254px){#D:before{content:"1254";}} @media (min-width:1255px){#D:before{content:"1255";}} @media (min-width:1256px){#D:before{content:"1256";}} @media (min-width:1257px){#D:before{content:"1257";}} @media (min-width:1258px){#D:before{content:"1258";}} @media (min-width:1259px){#D:before{content:"1259";}} @media (min-width:1260px){#D:before{content:"1260";}} @media (min-width:1261px){#D:before{content:"1261";}} @media (min-width:1262px){#D:before{content:"1262";}} @media (min-width:1263px){#D:before{content:"1263";}} @media (min-width:1264px){#D:before{content:"1264";}} @media (min-width:1265px){#D:before{content:"1265";}} @media (min-width:1266px){#D:before{content:"1266";}} @media (min-width:1267px){#D:before{content:"1267";}} @media (min-width:1268px){#D:before{content:"1268";}} @media (min-width:1269px){#D:before{content:"1269";}} @media (min-width:1270px){#D:before{content:"1270";}} @media (min-width:1271px){#D:before{content:"1271";}} @media (min-width:1272px){#D:before{content:"1272";}} @media (min-width:1273px){#D:before{content:"1273";}} @media (min-width:1274px){#D:before{content:"1274";}} @media (min-width:1275px){#D:before{content:"1275";}} @media (min-width:1276px){#D:before{content:"1276";}} @media (min-width:1277px){#D:before{content:"1277";}} @media (min-width:1278px){#D:before{content:"1278";}} @media (min-width:1279px){#D:before{content:"1279";}} @media (min-width:1280px){#D:before{content:"1280";}} @media (min-width:1281px){#D:before{content:"1281";}} @media (min-width:1282px){#D:before{content:"1282";}} @media (min-width:1283px){#D:before{content:"1283";}} @media (min-width:1284px){#D:before{content:"1284";}} @media (min-width:1285px){#D:before{content:"1285";}} @media (min-width:1286px){#D:before{content:"1286";}} @media (min-width:1287px){#D:before{content:"1287";}} @media (min-width:1288px){#D:before{content:"1288";}} @media (min-width:1289px){#D:before{content:"1289";}} @media (min-width:1290px){#D:before{content:"1290";}} @media (min-width:1291px){#D:before{content:"1291";}} @media (min-width:1292px){#D:before{content:"1292";}} @media (min-width:1293px){#D:before{content:"1293";}} @media (min-width:1294px){#D:before{content:"1294";}} @media (min-width:1295px){#D:before{content:"1295";}} @media (min-width:1296px){#D:before{content:"1296";}} @media (min-width:1297px){#D:before{content:"1297";}} @media (min-width:1298px){#D:before{content:"1298";}} @media (min-width:1299px){#D:before{content:"1299";}} @media (min-width:1300px){#D:before{content:"1300";}} @media (min-width:1301px){#D:before{content:"1301";}} @media (min-width:1302px){#D:before{content:"1302";}} @media (min-width:1303px){#D:before{content:"1303";}} @media (min-width:1304px){#D:before{content:"1304";}} @media (min-width:1305px){#D:before{content:"1305";}} @media (min-width:1306px){#D:before{content:"1306";}} @media (min-width:1307px){#D:before{content:"1307";}} @media (min-width:1308px){#D:before{content:"1308";}} @media (min-width:1309px){#D:before{content:"1309";}} @media (min-width:1310px){#D:before{content:"1310";}} @media (min-width:1311px){#D:before{content:"1311";}} @media (min-width:1312px){#D:before{content:"1312";}} @media (min-width:1313px){#D:before{content:"1313";}} @media (min-width:1314px){#D:before{content:"1314";}} @media (min-width:1315px){#D:before{content:"1315";}} @media (min-width:1316px){#D:before{content:"1316";}} @media (min-width:1317px){#D:before{content:"1317";}} @media (min-width:1318px){#D:before{content:"1318";}} @media (min-width:1319px){#D:before{content:"1319";}} @media (min-width:1320px){#D:before{content:"1320";}} @media (min-width:1321px){#D:before{content:"1321";}} @media (min-width:1322px){#D:before{content:"1322";}} @media (min-width:1323px){#D:before{content:"1323";}} @media (min-width:1324px){#D:before{content:"1324";}} @media (min-width:1325px){#D:before{content:"1325";}} @media (min-width:1326px){#D:before{content:"1326";}} @media (min-width:1327px){#D:before{content:"1327";}} @media (min-width:1328px){#D:before{content:"1328";}} @media (min-width:1329px){#D:before{content:"1329";}} @media (min-width:1330px){#D:before{content:"1330";}} @media (min-width:1331px){#D:before{content:"1331";}} @media (min-width:1332px){#D:before{content:"1332";}} @media (min-width:1333px){#D:before{content:"1333";}} @media (min-width:1334px){#D:before{content:"1334";}} @media (min-width:1335px){#D:before{content:"1335";}} @media (min-width:1336px){#D:before{content:"1336";}} @media (min-width:1337px){#D:before{content:"1337";}} @media (min-width:1338px){#D:before{content:"1338";}} @media (min-width:1339px){#D:before{content:"1339";}} @media (min-width:1340px){#D:before{content:"1340";}} @media (min-width:1341px){#D:before{content:"1341";}} @media (min-width:1342px){#D:before{content:"1342";}} @media (min-width:1343px){#D:before{content:"1343";}} @media (min-width:1344px){#D:before{content:"1344";}} @media (min-width:1345px){#D:before{content:"1345";}} @media (min-width:1346px){#D:before{content:"1346";}} @media (min-width:1347px){#D:before{content:"1347";}} @media (min-width:1348px){#D:before{content:"1348";}} @media (min-width:1349px){#D:before{content:"1349";}} @media (min-width:1350px){#D:before{content:"1350";}} @media (min-width:1351px){#D:before{content:"1351";}} @media (min-width:1352px){#D:before{content:"1352";}} @media (min-width:1353px){#D:before{content:"1353";}} @media (min-width:1354px){#D:before{content:"1354";}} @media (min-width:1355px){#D:before{content:"1355";}} @media (min-width:1356px){#D:before{content:"1356";}} @media (min-width:1357px){#D:before{content:"1357";}} @media (min-width:1358px){#D:before{content:"1358";}} @media (min-width:1359px){#D:before{content:"1359";}} @media (min-width:1360px){#D:before{content:"1360";}} @media (min-width:1361px){#D:before{content:"1361";}} @media (min-width:1362px){#D:before{content:"1362";}} @media (min-width:1363px){#D:before{content:"1363";}} @media (min-width:1364px){#D:before{content:"1364";}} @media (min-width:1365px){#D:before{content:"1365";}} @media (min-width:1366px){#D:before{content:"1366";}} @media (min-width:1367px){#D:before{content:"1367";}} @media (min-width:1368px){#D:before{content:"1368";}} @media (min-width:1369px){#D:before{content:"1369";}} @media (min-width:1370px){#D:before{content:"1370";}} @media (min-width:1371px){#D:before{content:"1371";}} @media (min-width:1372px){#D:before{content:"1372";}} @media (min-width:1373px){#D:before{content:"1373";}} @media (min-width:1374px){#D:before{content:"1374";}} @media (min-width:1375px){#D:before{content:"1375";}} @media (min-width:1376px){#D:before{content:"1376";}} @media (min-width:1377px){#D:before{content:"1377";}} @media (min-width:1378px){#D:before{content:"1378";}} @media (min-width:1379px){#D:before{content:"1379";}} @media (min-width:1380px){#D:before{content:"1380";}} @media (min-width:1381px){#D:before{content:"1381";}} @media (min-width:1382px){#D:before{content:"1382";}} @media (min-width:1383px){#D:before{content:"1383";}} @media (min-width:1384px){#D:before{content:"1384";}} @media (min-width:1385px){#D:before{content:"1385";}} @media (min-width:1386px){#D:before{content:"1386";}} @media (min-width:1387px){#D:before{content:"1387";}} @media (min-width:1388px){#D:before{content:"1388";}} @media (min-width:1389px){#D:before{content:"1389";}} @media (min-width:1390px){#D:before{content:"1390";}} @media (min-width:1391px){#D:before{content:"1391";}} @media (min-width:1392px){#D:before{content:"1392";}} @media (min-width:1393px){#D:before{content:"1393";}} @media (min-width:1394px){#D:before{content:"1394";}} @media (min-width:1395px){#D:before{content:"1395";}} @media (min-width:1396px){#D:before{content:"1396";}} @media (min-width:1397px){#D:before{content:"1397";}} @media (min-width:1398px){#D:before{content:"1398";}} @media (min-width:1399px){#D:before{content:"1399";}} @media (min-width:1400px){#D:before{content:"1400";}} @media (min-width:1401px){#D:before{content:"1401";}} @media (min-width:1402px){#D:before{content:"1402";}} @media (min-width:1403px){#D:before{content:"1403";}} @media (min-width:1404px){#D:before{content:"1404";}} @media (min-width:1405px){#D:before{content:"1405";}} @media (min-width:1406px){#D:before{content:"1406";}} @media (min-width:1407px){#D:before{content:"1407";}} @media (min-width:1408px){#D:before{content:"1408";}} @media (min-width:1409px){#D:before{content:"1409";}} @media (min-width:1410px){#D:before{content:"1410";}} @media (min-width:1411px){#D:before{content:"1411";}} @media (min-width:1412px){#D:before{content:"1412";}} @media (min-width:1413px){#D:before{content:"1413";}} @media (min-width:1414px){#D:before{content:"1414";}} @media (min-width:1415px){#D:before{content:"1415";}} @media (min-width:1416px){#D:before{content:"1416";}} @media (min-width:1417px){#D:before{content:"1417";}} @media (min-width:1418px){#D:before{content:"1418";}} @media (min-width:1419px){#D:before{content:"1419";}} @media (min-width:1420px){#D:before{content:"1420";}} @media (min-width:1421px){#D:before{content:"1421";}} @media (min-width:1422px){#D:before{content:"1422";}} @media (min-width:1423px){#D:before{content:"1423";}} @media (min-width:1424px){#D:before{content:"1424";}} @media (min-width:1425px){#D:before{content:"1425";}} @media (min-width:1426px){#D:before{content:"1426";}} @media (min-width:1427px){#D:before{content:"1427";}} @media (min-width:1428px){#D:before{content:"1428";}} @media (min-width:1429px){#D:before{content:"1429";}} @media (min-width:1430px){#D:before{content:"1430";}} @media (min-width:1431px){#D:before{content:"1431";}} @media (min-width:1432px){#D:before{content:"1432";}} @media (min-width:1433px){#D:before{content:"1433";}} @media (min-width:1434px){#D:before{content:"1434";}} @media (min-width:1435px){#D:before{content:"1435";}} @media (min-width:1436px){#D:before{content:"1436";}} @media (min-width:1437px){#D:before{content:"1437";}} @media (min-width:1438px){#D:before{content:"1438";}} @media (min-width:1439px){#D:before{content:"1439";}} @media (min-width:1440px){#D:before{content:"1440";}} @media (min-width:1441px){#D:before{content:"1441";}} @media (min-width:1442px){#D:before{content:"1442";}} @media (min-width:1443px){#D:before{content:"1443";}} @media (min-width:1444px){#D:before{content:"1444";}} @media (min-width:1445px){#D:before{content:"1445";}} @media (min-width:1446px){#D:before{content:"1446";}} @media (min-width:1447px){#D:before{content:"1447";}} @media (min-width:1448px){#D:before{content:"1448";}} @media (min-width:1449px){#D:before{content:"1449";}} @media (min-width:1450px){#D:before{content:"1450";}} @media (min-width:1451px){#D:before{content:"1451";}} @media (min-width:1452px){#D:before{content:"1452";}} @media (min-width:1453px){#D:before{content:"1453";}} @media (min-width:1454px){#D:before{content:"1454";}} @media (min-width:1455px){#D:before{content:"1455";}} @media (min-width:1456px){#D:before{content:"1456";}} @media (min-width:1457px){#D:before{content:"1457";}} @media (min-width:1458px){#D:before{content:"1458";}} @media (min-width:1459px){#D:before{content:"1459";}} @media (min-width:1460px){#D:before{content:"1460";}} @media (min-width:1461px){#D:before{content:"1461";}} @media (min-width:1462px){#D:before{content:"1462";}} @media (min-width:1463px){#D:before{content:"1463";}} @media (min-width:1464px){#D:before{content:"1464";}} @media (min-width:1465px){#D:before{content:"1465";}} @media (min-width:1466px){#D:before{content:"1466";}} @media (min-width:1467px){#D:before{content:"1467";}} @media (min-width:1468px){#D:before{content:"1468";}} @media (min-width:1469px){#D:before{content:"1469";}} @media (min-width:1470px){#D:before{content:"1470";}} @media (min-width:1471px){#D:before{content:"1471";}} @media (min-width:1472px){#D:before{content:"1472";}} @media (min-width:1473px){#D:before{content:"1473";}} @media (min-width:1474px){#D:before{content:"1474";}} @media (min-width:1475px){#D:before{content:"1475";}} @media (min-width:1476px){#D:before{content:"1476";}} @media (min-width:1477px){#D:before{content:"1477";}} @media (min-width:1478px){#D:before{content:"1478";}} @media (min-width:1479px){#D:before{content:"1479";}} @media (min-width:1480px){#D:before{content:"1480";}} @media (min-width:1481px){#D:before{content:"1481";}} @media (min-width:1482px){#D:before{content:"1482";}} @media (min-width:1483px){#D:before{content:"1483";}} @media (min-width:1484px){#D:before{content:"1484";}} @media (min-width:1485px){#D:before{content:"1485";}} @media (min-width:1486px){#D:before{content:"1486";}} @media (min-width:1487px){#D:before{content:"1487";}} @media (min-width:1488px){#D:before{content:"1488";}} @media (min-width:1489px){#D:before{content:"1489";}} @media (min-width:1490px){#D:before{content:"1490";}} @media (min-width:1491px){#D:before{content:"1491";}} @media (min-width:1492px){#D:before{content:"1492";}} @media (min-width:1493px){#D:before{content:"1493";}} @media (min-width:1494px){#D:before{content:"1494";}} @media (min-width:1495px){#D:before{content:"1495";}} @media (min-width:1496px){#D:before{content:"1496";}} @media (min-width:1497px){#D:before{content:"1497";}} @media (min-width:1498px){#D:before{content:"1498";}} @media (min-width:1499px){#D:before{content:"1499";}} @media (min-width:1500px){#D:before{content:"1500";}} @media (min-width:1501px){#D:before{content:"1501";}} @media (min-width:1502px){#D:before{content:"1502";}} @media (min-width:1503px){#D:before{content:"1503";}} @media (min-width:1504px){#D:before{content:"1504";}} @media (min-width:1505px){#D:before{content:"1505";}} @media (min-width:1506px){#D:before{content:"1506";}} @media (min-width:1507px){#D:before{content:"1507";}} @media (min-width:1508px){#D:before{content:"1508";}} @media (min-width:1509px){#D:before{content:"1509";}} @media (min-width:1510px){#D:before{content:"1510";}} @media (min-width:1511px){#D:before{content:"1511";}} @media (min-width:1512px){#D:before{content:"1512";}} @media (min-width:1513px){#D:before{content:"1513";}} @media (min-width:1514px){#D:before{content:"1514";}} @media (min-width:1515px){#D:before{content:"1515";}} @media (min-width:1516px){#D:before{content:"1516";}} @media (min-width:1517px){#D:before{content:"1517";}} @media (min-width:1518px){#D:before{content:"1518";}} @media (min-width:1519px){#D:before{content:"1519";}} @media (min-width:1520px){#D:before{content:"1520";}} @media (min-width:1521px){#D:before{content:"1521";}} @media (min-width:1522px){#D:before{content:"1522";}} @media (min-width:1523px){#D:before{content:"1523";}} @media (min-width:1524px){#D:before{content:"1524";}} @media (min-width:1525px){#D:before{content:"1525";}} @media (min-width:1526px){#D:before{content:"1526";}} @media (min-width:1527px){#D:before{content:"1527";}} @media (min-width:1528px){#D:before{content:"1528";}} @media (min-width:1529px){#D:before{content:"1529";}} @media (min-width:1530px){#D:before{content:"1530";}} @media (min-width:1531px){#D:before{content:"1531";}} @media (min-width:1532px){#D:before{content:"1532";}} @media (min-width:1533px){#D:before{content:"1533";}} @media (min-width:1534px){#D:before{content:"1534";}} @media (min-width:1535px){#D:before{content:"1535";}} @media (min-width:1536px){#D:before{content:"1536";}} @media (min-width:1537px){#D:before{content:"1537";}} @media (min-width:1538px){#D:before{content:"1538";}} @media (min-width:1539px){#D:before{content:"1539";}} @media (min-width:1540px){#D:before{content:"1540";}} @media (min-width:1541px){#D:before{content:"1541";}} @media (min-width:1542px){#D:before{content:"1542";}} @media (min-width:1543px){#D:before{content:"1543";}} @media (min-width:1544px){#D:before{content:"1544";}} @media (min-width:1545px){#D:before{content:"1545";}} @media (min-width:1546px){#D:before{content:"1546";}} @media (min-width:1547px){#D:before{content:"1547";}} @media (min-width:1548px){#D:before{content:"1548";}} @media (min-width:1549px){#D:before{content:"1549";}} @media (min-width:1550px){#D:before{content:"1550";}} @media (min-width:1551px){#D:before{content:"1551";}} @media (min-width:1552px){#D:before{content:"1552";}} @media (min-width:1553px){#D:before{content:"1553";}} @media (min-width:1554px){#D:before{content:"1554";}} @media (min-width:1555px){#D:before{content:"1555";}} @media (min-width:1556px){#D:before{content:"1556";}} @media (min-width:1557px){#D:before{content:"1557";}} @media (min-width:1558px){#D:before{content:"1558";}} @media (min-width:1559px){#D:before{content:"1559";}} @media (min-width:1560px){#D:before{content:"1560";}} @media (min-width:1561px){#D:before{content:"1561";}} @media (min-width:1562px){#D:before{content:"1562";}} @media (min-width:1563px){#D:before{content:"1563";}} @media (min-width:1564px){#D:before{content:"1564";}} @media (min-width:1565px){#D:before{content:"1565";}} @media (min-width:1566px){#D:before{content:"1566";}} @media (min-width:1567px){#D:before{content:"1567";}} @media (min-width:1568px){#D:before{content:"1568";}} @media (min-width:1569px){#D:before{content:"1569";}} @media (min-width:1570px){#D:before{content:"1570";}} @media (min-width:1571px){#D:before{content:"1571";}} @media (min-width:1572px){#D:before{content:"1572";}} @media (min-width:1573px){#D:before{content:"1573";}} @media (min-width:1574px){#D:before{content:"1574";}} @media (min-width:1575px){#D:before{content:"1575";}} @media (min-width:1576px){#D:before{content:"1576";}} @media (min-width:1577px){#D:before{content:"1577";}} @media (min-width:1578px){#D:before{content:"1578";}} @media (min-width:1579px){#D:before{content:"1579";}} @media (min-width:1580px){#D:before{content:"1580";}} @media (min-width:1581px){#D:before{content:"1581";}} @media (min-width:1582px){#D:before{content:"1582";}} @media (min-width:1583px){#D:before{content:"1583";}} @media (min-width:1584px){#D:before{content:"1584";}} @media (min-width:1585px){#D:before{content:"1585";}} @media (min-width:1586px){#D:before{content:"1586";}} @media (min-width:1587px){#D:before{content:"1587";}} @media (min-width:1588px){#D:before{content:"1588";}} @media (min-width:1589px){#D:before{content:"1589";}} @media (min-width:1590px){#D:before{content:"1590";}} @media (min-width:1591px){#D:before{content:"1591";}} @media (min-width:1592px){#D:before{content:"1592";}} @media (min-width:1593px){#D:before{content:"1593";}} @media (min-width:1594px){#D:before{content:"1594";}} @media (min-width:1595px){#D:before{content:"1595";}} @media (min-width:1596px){#D:before{content:"1596";}} @media (min-width:1597px){#D:before{content:"1597";}} @media (min-width:1598px){#D:before{content:"1598";}} @media (min-width:1599px){#D:before{content:"1599";}} @media (min-width:1600px){#D:before{content:"1600";}} @media (min-width:1601px){#D:before{content:"1601";}} @media (min-width:1602px){#D:before{content:"1602";}} @media (min-width:1603px){#D:before{content:"1603";}} @media (min-width:1604px){#D:before{content:"1604";}} @media (min-width:1605px){#D:before{content:"1605";}} @media (min-width:1606px){#D:before{content:"1606";}} @media (min-width:1607px){#D:before{content:"1607";}} @media (min-width:1608px){#D:before{content:"1608";}} @media (min-width:1609px){#D:before{content:"1609";}} @media (min-width:1610px){#D:before{content:"1610";}} @media (min-width:1611px){#D:before{content:"1611";}} @media (min-width:1612px){#D:before{content:"1612";}} @media (min-width:1613px){#D:before{content:"1613";}} @media (min-width:1614px){#D:before{content:"1614";}} @media (min-width:1615px){#D:before{content:"1615";}} @media (min-width:1616px){#D:before{content:"1616";}} @media (min-width:1617px){#D:before{content:"1617";}} @media (min-width:1618px){#D:before{content:"1618";}} @media (min-width:1619px){#D:before{content:"1619";}} @media (min-width:1620px){#D:before{content:"1620";}} @media (min-width:1621px){#D:before{content:"1621";}} @media (min-width:1622px){#D:before{content:"1622";}} @media (min-width:1623px){#D:before{content:"1623";}} @media (min-width:1624px){#D:before{content:"1624";}} @media (min-width:1625px){#D:before{content:"1625";}} @media (min-width:1626px){#D:before{content:"1626";}} @media (min-width:1627px){#D:before{content:"1627";}} @media (min-width:1628px){#D:before{content:"1628";}} @media (min-width:1629px){#D:before{content:"1629";}} @media (min-width:1630px){#D:before{content:"1630";}} @media (min-width:1631px){#D:before{content:"1631";}} @media (min-width:1632px){#D:before{content:"1632";}} @media (min-width:1633px){#D:before{content:"1633";}} @media (min-width:1634px){#D:before{content:"1634";}} @media (min-width:1635px){#D:before{content:"1635";}} @media (min-width:1636px){#D:before{content:"1636";}} @media (min-width:1637px){#D:before{content:"1637";}} @media (min-width:1638px){#D:before{content:"1638";}} @media (min-width:1639px){#D:before{content:"1639";}} @media (min-width:1640px){#D:before{content:"1640";}} @media (min-width:1641px){#D:before{content:"1641";}} @media (min-width:1642px){#D:before{content:"1642";}} @media (min-width:1643px){#D:before{content:"1643";}} @media (min-width:1644px){#D:before{content:"1644";}} @media (min-width:1645px){#D:before{content:"1645";}} @media (min-width:1646px){#D:before{content:"1646";}} @media (min-width:1647px){#D:before{content:"1647";}} @media (min-width:1648px){#D:before{content:"1648";}} @media (min-width:1649px){#D:before{content:"1649";}} @media (min-width:1650px){#D:before{content:"1650";}} @media (min-width:1651px){#D:before{content:"1651";}} @media (min-width:1652px){#D:before{content:"1652";}} @media (min-width:1653px){#D:before{content:"1653";}} @media (min-width:1654px){#D:before{content:"1654";}} @media (min-width:1655px){#D:before{content:"1655";}} @media (min-width:1656px){#D:before{content:"1656";}} @media (min-width:1657px){#D:before{content:"1657";}} @media (min-width:1658px){#D:before{content:"1658";}} @media (min-width:1659px){#D:before{content:"1659";}} @media (min-width:1660px){#D:before{content:"1660";}} @media (min-width:1661px){#D:before{content:"1661";}} @media (min-width:1662px){#D:before{content:"1662";}} @media (min-width:1663px){#D:before{content:"1663";}} @media (min-width:1664px){#D:before{content:"1664";}} @media (min-width:1665px){#D:before{content:"1665";}} @media (min-width:1666px){#D:before{content:"1666";}} @media (min-width:1667px){#D:before{content:"1667";}} @media (min-width:1668px){#D:before{content:"1668";}} @media (min-width:1669px){#D:before{content:"1669";}} @media (min-width:1670px){#D:before{content:"1670";}} @media (min-width:1671px){#D:before{content:"1671";}} @media (min-width:1672px){#D:before{content:"1672";}} @media (min-width:1673px){#D:before{content:"1673";}} @media (min-width:1674px){#D:before{content:"1674";}} @media (min-width:1675px){#D:before{content:"1675";}} @media (min-width:1676px){#D:before{content:"1676";}} @media (min-width:1677px){#D:before{content:"1677";}} @media (min-width:1678px){#D:before{content:"1678";}} @media (min-width:1679px){#D:before{content:"1679";}} @media (min-width:1680px){#D:before{content:"1680";}} @media (min-width:1681px){#D:before{content:"1681";}} @media (min-width:1682px){#D:before{content:"1682";}} @media (min-width:1683px){#D:before{content:"1683";}} @media (min-width:1684px){#D:before{content:"1684";}} @media (min-width:1685px){#D:before{content:"1685";}} @media (min-width:1686px){#D:before{content:"1686";}} @media (min-width:1687px){#D:before{content:"1687";}} @media (min-width:1688px){#D:before{content:"1688";}} @media (min-width:1689px){#D:before{content:"1689";}} @media (min-width:1690px){#D:before{content:"1690";}} @media (min-width:1691px){#D:before{content:"1691";}} @media (min-width:1692px){#D:before{content:"1692";}} @media (min-width:1693px){#D:before{content:"1693";}} @media (min-width:1694px){#D:before{content:"1694";}} @media (min-width:1695px){#D:before{content:"1695";}} @media (min-width:1696px){#D:before{content:"1696";}} @media (min-width:1697px){#D:before{content:"1697";}} @media (min-width:1698px){#D:before{content:"1698";}} @media (min-width:1699px){#D:before{content:"1699";}} @media (min-width:1700px){#D:before{content:"1700";}} @media (min-width:1701px){#D:before{content:"1701";}} @media (min-width:1702px){#D:before{content:"1702";}} @media (min-width:1703px){#D:before{content:"1703";}} @media (min-width:1704px){#D:before{content:"1704";}} @media (min-width:1705px){#D:before{content:"1705";}} @media (min-width:1706px){#D:before{content:"1706";}} @media (min-width:1707px){#D:before{content:"1707";}} @media (min-width:1708px){#D:before{content:"1708";}} @media (min-width:1709px){#D:before{content:"1709";}} @media (min-width:1710px){#D:before{content:"1710";}} @media (min-width:1711px){#D:before{content:"1711";}} @media (min-width:1712px){#D:before{content:"1712";}} @media (min-width:1713px){#D:before{content:"1713";}} @media (min-width:1714px){#D:before{content:"1714";}} @media (min-width:1715px){#D:before{content:"1715";}} @media (min-width:1716px){#D:before{content:"1716";}} @media (min-width:1717px){#D:before{content:"1717";}} @media (min-width:1718px){#D:before{content:"1718";}} @media (min-width:1719px){#D:before{content:"1719";}} @media (min-width:1720px){#D:before{content:"1720";}} @media (min-width:1721px){#D:before{content:"1721";}} @media (min-width:1722px){#D:before{content:"1722";}} @media (min-width:1723px){#D:before{content:"1723";}} @media (min-width:1724px){#D:before{content:"1724";}} @media (min-width:1725px){#D:before{content:"1725";}} @media (min-width:1726px){#D:before{content:"1726";}} @media (min-width:1727px){#D:before{content:"1727";}} @media (min-width:1728px){#D:before{content:"1728";}} @media (min-width:1729px){#D:before{content:"1729";}} @media (min-width:1730px){#D:before{content:"1730";}} @media (min-width:1731px){#D:before{content:"1731";}} @media (min-width:1732px){#D:before{content:"1732";}} @media (min-width:1733px){#D:before{content:"1733";}} @media (min-width:1734px){#D:before{content:"1734";}} @media (min-width:1735px){#D:before{content:"1735";}} @media (min-width:1736px){#D:before{content:"1736";}} @media (min-width:1737px){#D:before{content:"1737";}} @media (min-width:1738px){#D:before{content:"1738";}} @media (min-width:1739px){#D:before{content:"1739";}} @media (min-width:1740px){#D:before{content:"1740";}} @media (min-width:1741px){#D:before{content:"1741";}} @media (min-width:1742px){#D:before{content:"1742";}} @media (min-width:1743px){#D:before{content:"1743";}} @media (min-width:1744px){#D:before{content:"1744";}} @media (min-width:1745px){#D:before{content:"1745";}} @media (min-width:1746px){#D:before{content:"1746";}} @media (min-width:1747px){#D:before{content:"1747";}} @media (min-width:1748px){#D:before{content:"1748";}} @media (min-width:1749px){#D:before{content:"1749";}} @media (min-width:1750px){#D:before{content:"1750";}} @media (min-width:1751px){#D:before{content:"1751";}} @media (min-width:1752px){#D:before{content:"1752";}} @media (min-width:1753px){#D:before{content:"1753";}} @media (min-width:1754px){#D:before{content:"1754";}} @media (min-width:1755px){#D:before{content:"1755";}} @media (min-width:1756px){#D:before{content:"1756";}} @media (min-width:1757px){#D:before{content:"1757";}} @media (min-width:1758px){#D:before{content:"1758";}} @media (min-width:1759px){#D:before{content:"1759";}} @media (min-width:1760px){#D:before{content:"1760";}} @media (min-width:1761px){#D:before{content:"1761";}} @media (min-width:1762px){#D:before{content:"1762";}} @media (min-width:1763px){#D:before{content:"1763";}} @media (min-width:1764px){#D:before{content:"1764";}} @media (min-width:1765px){#D:before{content:"1765";}} @media (min-width:1766px){#D:before{content:"1766";}} @media (min-width:1767px){#D:before{content:"1767";}} @media (min-width:1768px){#D:before{content:"1768";}} @media (min-width:1769px){#D:before{content:"1769";}} @media (min-width:1770px){#D:before{content:"1770";}} @media (min-width:1771px){#D:before{content:"1771";}} @media (min-width:1772px){#D:before{content:"1772";}} @media (min-width:1773px){#D:before{content:"1773";}} @media (min-width:1774px){#D:before{content:"1774";}} @media (min-width:1775px){#D:before{content:"1775";}} @media (min-width:1776px){#D:before{content:"1776";}} @media (min-width:1777px){#D:before{content:"1777";}} @media (min-width:1778px){#D:before{content:"1778";}} @media (min-width:1779px){#D:before{content:"1779";}} @media (min-width:1780px){#D:before{content:"1780";}} @media (min-width:1781px){#D:before{content:"1781";}} @media (min-width:1782px){#D:before{content:"1782";}} @media (min-width:1783px){#D:before{content:"1783";}} @media (min-width:1784px){#D:before{content:"1784";}} @media (min-width:1785px){#D:before{content:"1785";}} @media (min-width:1786px){#D:before{content:"1786";}} @media (min-width:1787px){#D:before{content:"1787";}} @media (min-width:1788px){#D:before{content:"1788";}} @media (min-width:1789px){#D:before{content:"1789";}} @media (min-width:1790px){#D:before{content:"1790";}} @media (min-width:1791px){#D:before{content:"1791";}} @media (min-width:1792px){#D:before{content:"1792";}} @media (min-width:1793px){#D:before{content:"1793";}} @media (min-width:1794px){#D:before{content:"1794";}} @media (min-width:1795px){#D:before{content:"1795";}} @media (min-width:1796px){#D:before{content:"1796";}} @media (min-width:1797px){#D:before{content:"1797";}} @media (min-width:1798px){#D:before{content:"1798";}} @media (min-width:1799px){#D:before{content:"1799";}} @media (min-width:1800px){#D:before{content:"1800";}} @media (min-width:1801px){#D:before{content:"1801";}} @media (min-width:1802px){#D:before{content:"1802";}} @media (min-width:1803px){#D:before{content:"1803";}} @media (min-width:1804px){#D:before{content:"1804";}} @media (min-width:1805px){#D:before{content:"1805";}} @media (min-width:1806px){#D:before{content:"1806";}} @media (min-width:1807px){#D:before{content:"1807";}} @media (min-width:1808px){#D:before{content:"1808";}} @media (min-width:1809px){#D:before{content:"1809";}} @media (min-width:1810px){#D:before{content:"1810";}} @media (min-width:1811px){#D:before{content:"1811";}} @media (min-width:1812px){#D:before{content:"1812";}} @media (min-width:1813px){#D:before{content:"1813";}} @media (min-width:1814px){#D:before{content:"1814";}} @media (min-width:1815px){#D:before{content:"1815";}} @media (min-width:1816px){#D:before{content:"1816";}} @media (min-width:1817px){#D:before{content:"1817";}} @media (min-width:1818px){#D:before{content:"1818";}} @media (min-width:1819px){#D:before{content:"1819";}} @media (min-width:1820px){#D:before{content:"1820";}} @media (min-width:1821px){#D:before{content:"1821";}} @media (min-width:1822px){#D:before{content:"1822";}} @media (min-width:1823px){#D:before{content:"1823";}} @media (min-width:1824px){#D:before{content:"1824";}} @media (min-width:1825px){#D:before{content:"1825";}} @media (min-width:1826px){#D:before{content:"1826";}} @media (min-width:1827px){#D:before{content:"1827";}} @media (min-width:1828px){#D:before{content:"1828";}} @media (min-width:1829px){#D:before{content:"1829";}} @media (min-width:1830px){#D:before{content:"1830";}} @media (min-width:1831px){#D:before{content:"1831";}} @media (min-width:1832px){#D:before{content:"1832";}} @media (min-width:1833px){#D:before{content:"1833";}} @media (min-width:1834px){#D:before{content:"1834";}} @media (min-width:1835px){#D:before{content:"1835";}} @media (min-width:1836px){#D:before{content:"1836";}} @media (min-width:1837px){#D:before{content:"1837";}} @media (min-width:1838px){#D:before{content:"1838";}} @media (min-width:1839px){#D:before{content:"1839";}} @media (min-width:1840px){#D:before{content:"1840";}} @media (min-width:1841px){#D:before{content:"1841";}} @media (min-width:1842px){#D:before{content:"1842";}} @media (min-width:1843px){#D:before{content:"1843";}} @media (min-width:1844px){#D:before{content:"1844";}} @media (min-width:1845px){#D:before{content:"1845";}} @media (min-width:1846px){#D:before{content:"1846";}} @media (min-width:1847px){#D:before{content:"1847";}} @media (min-width:1848px){#D:before{content:"1848";}} @media (min-width:1849px){#D:before{content:"1849";}} @media (min-width:1850px){#D:before{content:"1850";}} @media (min-width:1851px){#D:before{content:"1851";}} @media (min-width:1852px){#D:before{content:"1852";}} @media (min-width:1853px){#D:before{content:"1853";}} @media (min-width:1854px){#D:before{content:"1854";}} @media (min-width:1855px){#D:before{content:"1855";}} @media (min-width:1856px){#D:before{content:"1856";}} @media (min-width:1857px){#D:before{content:"1857";}} @media (min-width:1858px){#D:before{content:"1858";}} @media (min-width:1859px){#D:before{content:"1859";}} @media (min-width:1860px){#D:before{content:"1860";}} @media (min-width:1861px){#D:before{content:"1861";}} @media (min-width:1862px){#D:before{content:"1862";}} @media (min-width:1863px){#D:before{content:"1863";}} @media (min-width:1864px){#D:before{content:"1864";}} @media (min-width:1865px){#D:before{content:"1865";}} @media (min-width:1866px){#D:before{content:"1866";}} @media (min-width:1867px){#D:before{content:"1867";}} @media (min-width:1868px){#D:before{content:"1868";}} @media (min-width:1869px){#D:before{content:"1869";}} @media (min-width:1870px){#D:before{content:"1870";}} @media (min-width:1871px){#D:before{content:"1871";}} @media (min-width:1872px){#D:before{content:"1872";}} @media (min-width:1873px){#D:before{content:"1873";}} @media (min-width:1874px){#D:before{content:"1874";}} @media (min-width:1875px){#D:before{content:"1875";}} @media (min-width:1876px){#D:before{content:"1876";}} @media (min-width:1877px){#D:before{content:"1877";}} @media (min-width:1878px){#D:before{content:"1878";}} @media (min-width:1879px){#D:before{content:"1879";}} @media (min-width:1880px){#D:before{content:"1880";}} @media (min-width:1881px){#D:before{content:"1881";}} @media (min-width:1882px){#D:before{content:"1882";}} @media (min-width:1883px){#D:before{content:"1883";}} @media (min-width:1884px){#D:before{content:"1884";}} @media (min-width:1885px){#D:before{content:"1885";}} @media (min-width:1886px){#D:before{content:"1886";}} @media (min-width:1887px){#D:before{content:"1887";}} @media (min-width:1888px){#D:before{content:"1888";}} @media (min-width:1889px){#D:before{content:"1889";}} @media (min-width:1890px){#D:before{content:"1890";}} @media (min-width:1891px){#D:before{content:"1891";}} @media (min-width:1892px){#D:before{content:"1892";}} @media (min-width:1893px){#D:before{content:"1893";}} @media (min-width:1894px){#D:before{content:"1894";}} @media (min-width:1895px){#D:before{content:"1895";}} @media (min-width:1896px){#D:before{content:"1896";}} @media (min-width:1897px){#D:before{content:"1897";}} @media (min-width:1898px){#D:before{content:"1898";}} @media (min-width:1899px){#D:before{content:"1899";}} @media (min-width:1900px){#D:before{content:"1900";}} @media (min-width:1901px){#D:before{content:"1901";}} @media (min-width:1902px){#D:before{content:"1902";}} @media (min-width:1903px){#D:before{content:"1903";}} @media (min-width:1904px){#D:before{content:"1904";}} @media (min-width:1905px){#D:before{content:"1905";}} @media (min-width:1906px){#D:before{content:"1906";}} @media (min-width:1907px){#D:before{content:"1907";}} @media (min-width:1908px){#D:before{content:"1908";}} @media (min-width:1909px){#D:before{content:"1909";}} @media (min-width:1910px){#D:before{content:"1910";}} @media (min-width:1911px){#D:before{content:"1911";}} @media (min-width:1912px){#D:before{content:"1912";}} @media (min-width:1913px){#D:before{content:"1913";}} @media (min-width:1914px){#D:before{content:"1914";}} @media (min-width:1915px){#D:before{content:"1915";}} @media (min-width:1916px){#D:before{content:"1916";}} @media (min-width:1917px){#D:before{content:"1917";}} @media (min-width:1918px){#D:before{content:"1918";}} @media (min-width:1919px){#D:before{content:"1919";}} @media (min-width:1920px){#D:before{content:"1920";}} @media (min-width:1921px){#D:before{content:"1921";}} @media (min-width:1922px){#D:before{content:"1922";}} @media (min-width:1923px){#D:before{content:"1923";}} @media (min-width:1924px){#D:before{content:"1924";}} @media (min-width:1925px){#D:before{content:"1925";}} @media (min-width:1926px){#D:before{content:"1926";}} @media (min-width:1927px){#D:before{content:"1927";}} @media (min-width:1928px){#D:before{content:"1928";}} @media (min-width:1929px){#D:before{content:"1929";}} @media (min-width:1930px){#D:before{content:"1930";}} @media (min-width:1931px){#D:before{content:"1931";}} @media (min-width:1932px){#D:before{content:"1932";}} @media (min-width:1933px){#D:before{content:"1933";}} @media (min-width:1934px){#D:before{content:"1934";}} @media (min-width:1935px){#D:before{content:"1935";}} @media (min-width:1936px){#D:before{content:"1936";}} @media (min-width:1937px){#D:before{content:"1937";}} @media (min-width:1938px){#D:before{content:"1938";}} @media (min-width:1939px){#D:before{content:"1939";}} @media (min-width:1940px){#D:before{content:"1940";}} @media (min-width:1941px){#D:before{content:"1941";}} @media (min-width:1942px){#D:before{content:"1942";}} @media (min-width:1943px){#D:before{content:"1943";}} @media (min-width:1944px){#D:before{content:"1944";}} @media (min-width:1945px){#D:before{content:"1945";}} @media (min-width:1946px){#D:before{content:"1946";}} @media (min-width:1947px){#D:before{content:"1947";}} @media (min-width:1948px){#D:before{content:"1948";}} @media (min-width:1949px){#D:before{content:"1949";}} @media (min-width:1950px){#D:before{content:"1950";}} @media (min-width:1951px){#D:before{content:"1951";}} @media (min-width:1952px){#D:before{content:"1952";}} @media (min-width:1953px){#D:before{content:"1953";}} @media (min-width:1954px){#D:before{content:"1954";}} @media (min-width:1955px){#D:before{content:"1955";}} @media (min-width:1956px){#D:before{content:"1956";}} @media (min-width:1957px){#D:before{content:"1957";}} @media (min-width:1958px){#D:before{content:"1958";}} @media (min-width:1959px){#D:before{content:"1959";}} @media (min-width:1960px){#D:before{content:"1960";}} @media (min-width:1961px){#D:before{content:"1961";}} @media (min-width:1962px){#D:before{content:"1962";}} @media (min-width:1963px){#D:before{content:"1963";}} @media (min-width:1964px){#D:before{content:"1964";}} @media (min-width:1965px){#D:before{content:"1965";}} @media (min-width:1966px){#D:before{content:"1966";}} @media (min-width:1967px){#D:before{content:"1967";}} @media (min-width:1968px){#D:before{content:"1968";}} @media (min-width:1969px){#D:before{content:"1969";}} @media (min-width:1970px){#D:before{content:"1970";}} @media (min-width:1971px){#D:before{content:"1971";}} @media (min-width:1972px){#D:before{content:"1972";}} @media (min-width:1973px){#D:before{content:"1973";}} @media (min-width:1974px){#D:before{content:"1974";}} @media (min-width:1975px){#D:before{content:"1975";}} @media (min-width:1976px){#D:before{content:"1976";}} @media (min-width:1977px){#D:before{content:"1977";}} @media (min-width:1978px){#D:before{content:"1978";}} @media (min-width:1979px){#D:before{content:"1979";}} @media (min-width:1980px){#D:before{content:"1980";}} @media (min-width:1981px){#D:before{content:"1981";}} @media (min-width:1982px){#D:before{content:"1982";}} @media (min-width:1983px){#D:before{content:"1983";}} @media (min-width:1984px){#D:before{content:"1984";}} @media (min-width:1985px){#D:before{content:"1985";}} @media (min-width:1986px){#D:before{content:"1986";}} @media (min-width:1987px){#D:before{content:"1987";}} @media (min-width:1988px){#D:before{content:"1988";}} @media (min-width:1989px){#D:before{content:"1989";}} @media (min-width:1990px){#D:before{content:"1990";}} @media (min-width:1991px){#D:before{content:"1991";}} @media (min-width:1992px){#D:before{content:"1992";}} @media (min-width:1993px){#D:before{content:"1993";}} @media (min-width:1994px){#D:before{content:"1994";}} @media (min-width:1995px){#D:before{content:"1995";}} @media (min-width:1996px){#D:before{content:"1996";}} @media (min-width:1997px){#D:before{content:"1997";}} @media (min-width:1998px){#D:before{content:"1998";}} @media (min-width:1999px){#D:before{content:"1999";}} @media (min-width:2000px){#D:before{content:"2000";}} @media (min-width:2001px){#D:before{content:"2001";}} @media (min-width:2002px){#D:before{content:"2002";}} @media (min-width:2003px){#D:before{content:"2003";}} @media (min-width:2004px){#D:before{content:"2004";}} @media (min-width:2005px){#D:before{content:"2005";}} @media (min-width:2006px){#D:before{content:"2006";}} @media (min-width:2007px){#D:before{content:"2007";}} @media (min-width:2008px){#D:before{content:"2008";}} @media (min-width:2009px){#D:before{content:"2009";}} @media (min-width:2010px){#D:before{content:"2010";}} @media (min-width:2011px){#D:before{content:"2011";}} @media (min-width:2012px){#D:before{content:"2012";}} @media (min-width:2013px){#D:before{content:"2013";}} @media (min-width:2014px){#D:before{content:"2014";}} @media (min-width:2015px){#D:before{content:"2015";}} @media (min-width:2016px){#D:before{content:"2016";}} @media (min-width:2017px){#D:before{content:"2017";}} @media (min-width:2018px){#D:before{content:"2018";}} @media (min-width:2019px){#D:before{content:"2019";}} @media (min-width:2020px){#D:before{content:"2020";}} @media (min-width:2021px){#D:before{content:"2021";}} @media (min-width:2022px){#D:before{content:"2022";}} @media (min-width:2023px){#D:before{content:"2023";}} @media (min-width:2024px){#D:before{content:"2024";}} @media (min-width:2025px){#D:before{content:"2025";}} @media (min-width:2026px){#D:before{content:"2026";}} @media (min-width:2027px){#D:before{content:"2027";}} @media (min-width:2028px){#D:before{content:"2028";}} @media (min-width:2029px){#D:before{content:"2029";}} @media (min-width:2030px){#D:before{content:"2030";}} @media (min-width:2031px){#D:before{content:"2031";}} @media (min-width:2032px){#D:before{content:"2032";}} @media (min-width:2033px){#D:before{content:"2033";}} @media (min-width:2034px){#D:before{content:"2034";}} @media (min-width:2035px){#D:before{content:"2035";}} @media (min-width:2036px){#D:before{content:"2036";}} @media (min-width:2037px){#D:before{content:"2037";}} @media (min-width:2038px){#D:before{content:"2038";}} @media (min-width:2039px){#D:before{content:"2039";}} @media (min-width:2040px){#D:before{content:"2040";}} @media (min-width:2041px){#D:before{content:"2041";}} @media (min-width:2042px){#D:before{content:"2042";}} @media (min-width:2043px){#D:before{content:"2043";}} @media (min-width:2044px){#D:before{content:"2044";}} @media (min-width:2045px){#D:before{content:"2045";}} @media (min-width:2046px){#D:before{content:"2046";}} @media (min-width:2047px){#D:before{content:"2047";}} @media (min-width:2048px){#D:before{content:"2048";}} @media (min-width:2049px){#D:before{content:"2049";}} @media (min-width:2050px){#D:before{content:"2050";}} @media (min-width:2051px){#D:before{content:"2051";}} @media (min-width:2052px){#D:before{content:"2052";}} @media (min-width:2053px){#D:before{content:"2053";}} @media (min-width:2054px){#D:before{content:"2054";}} @media (min-width:2055px){#D:before{content:"2055";}} @media (min-width:2056px){#D:before{content:"2056";}} @media (min-width:2057px){#D:before{content:"2057";}} @media (min-width:2058px){#D:before{content:"2058";}} @media (min-width:2059px){#D:before{content:"2059";}} @media (min-width:2060px){#D:before{content:"2060";}} @media (min-width:2061px){#D:before{content:"2061";}} @media (min-width:2062px){#D:before{content:"2062";}} @media (min-width:2063px){#D:before{content:"2063";}} @media (min-width:2064px){#D:before{content:"2064";}} @media (min-width:2065px){#D:before{content:"2065";}} @media (min-width:2066px){#D:before{content:"2066";}} @media (min-width:2067px){#D:before{content:"2067";}} @media (min-width:2068px){#D:before{content:"2068";}} @media (min-width:2069px){#D:before{content:"2069";}} @media (min-width:2070px){#D:before{content:"2070";}} @media (min-width:2071px){#D:before{content:"2071";}} @media (min-width:2072px){#D:before{content:"2072";}} @media (min-width:2073px){#D:before{content:"2073";}} @media (min-width:2074px){#D:before{content:"2074";}} @media (min-width:2075px){#D:before{content:"2075";}} @media (min-width:2076px){#D:before{content:"2076";}} @media (min-width:2077px){#D:before{content:"2077";}} @media (min-width:2078px){#D:before{content:"2078";}} @media (min-width:2079px){#D:before{content:"2079";}} @media (min-width:2080px){#D:before{content:"2080";}} @media (min-width:2081px){#D:before{content:"2081";}} @media (min-width:2082px){#D:before{content:"2082";}} @media (min-width:2083px){#D:before{content:"2083";}} @media (min-width:2084px){#D:before{content:"2084";}} @media (min-width:2085px){#D:before{content:"2085";}} @media (min-width:2086px){#D:before{content:"2086";}} @media (min-width:2087px){#D:before{content:"2087";}} @media (min-width:2088px){#D:before{content:"2088";}} @media (min-width:2089px){#D:before{content:"2089";}} @media (min-width:2090px){#D:before{content:"2090";}} @media (min-width:2091px){#D:before{content:"2091";}} @media (min-width:2092px){#D:before{content:"2092";}} @media (min-width:2093px){#D:before{content:"2093";}} @media (min-width:2094px){#D:before{content:"2094";}} @media (min-width:2095px){#D:before{content:"2095";}} @media (min-width:2096px){#D:before{content:"2096";}} @media (min-width:2097px){#D:before{content:"2097";}} @media (min-width:2098px){#D:before{content:"2098";}} @media (min-width:2099px){#D:before{content:"2099";}} @media (min-width:2100px){#D:before{content:"2100";}} @media (min-width:2101px){#D:before{content:"2101";}} @media (min-width:2102px){#D:before{content:"2102";}} @media (min-width:2103px){#D:before{content:"2103";}} @media (min-width:2104px){#D:before{content:"2104";}} @media (min-width:2105px){#D:before{content:"2105";}} @media (min-width:2106px){#D:before{content:"2106";}} @media (min-width:2107px){#D:before{content:"2107";}} @media (min-width:2108px){#D:before{content:"2108";}} @media (min-width:2109px){#D:before{content:"2109";}} @media (min-width:2110px){#D:before{content:"2110";}} @media (min-width:2111px){#D:before{content:"2111";}} @media (min-width:2112px){#D:before{content:"2112";}} @media (min-width:2113px){#D:before{content:"2113";}} @media (min-width:2114px){#D:before{content:"2114";}} @media (min-width:2115px){#D:before{content:"2115";}} @media (min-width:2116px){#D:before{content:"2116";}} @media (min-width:2117px){#D:before{content:"2117";}} @media (min-width:2118px){#D:before{content:"2118";}} @media (min-width:2119px){#D:before{content:"2119";}} @media (min-width:2120px){#D:before{content:"2120";}} @media (min-width:2121px){#D:before{content:"2121";}} @media (min-width:2122px){#D:before{content:"2122";}} @media (min-width:2123px){#D:before{content:"2123";}} @media (min-width:2124px){#D:before{content:"2124";}} @media (min-width:2125px){#D:before{content:"2125";}} @media (min-width:2126px){#D:before{content:"2126";}} @media (min-width:2127px){#D:before{content:"2127";}} @media (min-width:2128px){#D:before{content:"2128";}} @media (min-width:2129px){#D:before{content:"2129";}} @media (min-width:2130px){#D:before{content:"2130";}} @media (min-width:2131px){#D:before{content:"2131";}} @media (min-width:2132px){#D:before{content:"2132";}} @media (min-width:2133px){#D:before{content:"2133";}} @media (min-width:2134px){#D:before{content:"2134";}} @media (min-width:2135px){#D:before{content:"2135";}} @media (min-width:2136px){#D:before{content:"2136";}} @media (min-width:2137px){#D:before{content:"2137";}} @media (min-width:2138px){#D:before{content:"2138";}} @media (min-width:2139px){#D:before{content:"2139";}} @media (min-width:2140px){#D:before{content:"2140";}} @media (min-width:2141px){#D:before{content:"2141";}} @media (min-width:2142px){#D:before{content:"2142";}} @media (min-width:2143px){#D:before{content:"2143";}} @media (min-width:2144px){#D:before{content:"2144";}} @media (min-width:2145px){#D:before{content:"2145";}} @media (min-width:2146px){#D:before{content:"2146";}} @media (min-width:2147px){#D:before{content:"2147";}} @media (min-width:2148px){#D:before{content:"2148";}} @media (min-width:2149px){#D:before{content:"2149";}} @media (min-width:2150px){#D:before{content:"2150";}} @media (min-width:2151px){#D:before{content:"2151";}} @media (min-width:2152px){#D:before{content:"2152";}} @media (min-width:2153px){#D:before{content:"2153";}} @media (min-width:2154px){#D:before{content:"2154";}} @media (min-width:2155px){#D:before{content:"2155";}} @media (min-width:2156px){#D:before{content:"2156";}} @media (min-width:2157px){#D:before{content:"2157";}} @media (min-width:2158px){#D:before{content:"2158";}} @media (min-width:2159px){#D:before{content:"2159";}} @media (min-width:2160px){#D:before{content:"2160";}} @media (min-width:2161px){#D:before{content:"2161";}} @media (min-width:2162px){#D:before{content:"2162";}} @media (min-width:2163px){#D:before{content:"2163";}} @media (min-width:2164px){#D:before{content:"2164";}} @media (min-width:2165px){#D:before{content:"2165";}} @media (min-width:2166px){#D:before{content:"2166";}} @media (min-width:2167px){#D:before{content:"2167";}} @media (min-width:2168px){#D:before{content:"2168";}} @media (min-width:2169px){#D:before{content:"2169";}} @media (min-width:2170px){#D:before{content:"2170";}} @media (min-width:2171px){#D:before{content:"2171";}} @media (min-width:2172px){#D:before{content:"2172";}} @media (min-width:2173px){#D:before{content:"2173";}} @media (min-width:2174px){#D:before{content:"2174";}} @media (min-width:2175px){#D:before{content:"2175";}} @media (min-width:2176px){#D:before{content:"2176";}} @media (min-width:2177px){#D:before{content:"2177";}} @media (min-width:2178px){#D:before{content:"2178";}} @media (min-width:2179px){#D:before{content:"2179";}} @media (min-width:2180px){#D:before{content:"2180";}} @media (min-width:2181px){#D:before{content:"2181";}} @media (min-width:2182px){#D:before{content:"2182";}} @media (min-width:2183px){#D:before{content:"2183";}} @media (min-width:2184px){#D:before{content:"2184";}} @media (min-width:2185px){#D:before{content:"2185";}} @media (min-width:2186px){#D:before{content:"2186";}} @media (min-width:2187px){#D:before{content:"2187";}} @media (min-width:2188px){#D:before{content:"2188";}} @media (min-width:2189px){#D:before{content:"2189";}} @media (min-width:2190px){#D:before{content:"2190";}} @media (min-width:2191px){#D:before{content:"2191";}} @media (min-width:2192px){#D:before{content:"2192";}} @media (min-width:2193px){#D:before{content:"2193";}} @media (min-width:2194px){#D:before{content:"2194";}} @media (min-width:2195px){#D:before{content:"2195";}} @media (min-width:2196px){#D:before{content:"2196";}} @media (min-width:2197px){#D:before{content:"2197";}} @media (min-width:2198px){#D:before{content:"2198";}} @media (min-width:2199px){#D:before{content:"2199";}} @media (min-width:2200px){#D:before{content:"2200";}} @media (min-width:2201px){#D:before{content:"2201";}} @media (min-width:2202px){#D:before{content:"2202";}} @media (min-width:2203px){#D:before{content:"2203";}} @media (min-width:2204px){#D:before{content:"2204";}} @media (min-width:2205px){#D:before{content:"2205";}} @media (min-width:2206px){#D:before{content:"2206";}} @media (min-width:2207px){#D:before{content:"2207";}} @media (min-width:2208px){#D:before{content:"2208";}} @media (min-width:2209px){#D:before{content:"2209";}} @media (min-width:2210px){#D:before{content:"2210";}} @media (min-width:2211px){#D:before{content:"2211";}} @media (min-width:2212px){#D:before{content:"2212";}} @media (min-width:2213px){#D:before{content:"2213";}} @media (min-width:2214px){#D:before{content:"2214";}} @media (min-width:2215px){#D:before{content:"2215";}} @media (min-width:2216px){#D:before{content:"2216";}} @media (min-width:2217px){#D:before{content:"2217";}} @media (min-width:2218px){#D:before{content:"2218";}} @media (min-width:2219px){#D:before{content:"2219";}} @media (min-width:2220px){#D:before{content:"2220";}} @media (min-width:2221px){#D:before{content:"2221";}} @media (min-width:2222px){#D:before{content:"2222";}} @media (min-width:2223px){#D:before{content:"2223";}} @media (min-width:2224px){#D:before{content:"2224";}} @media (min-width:2225px){#D:before{content:"2225";}} @media (min-width:2226px){#D:before{content:"2226";}} @media (min-width:2227px){#D:before{content:"2227";}} @media (min-width:2228px){#D:before{content:"2228";}} @media (min-width:2229px){#D:before{content:"2229";}} @media (min-width:2230px){#D:before{content:"2230";}} @media (min-width:2231px){#D:before{content:"2231";}} @media (min-width:2232px){#D:before{content:"2232";}} @media (min-width:2233px){#D:before{content:"2233";}} @media (min-width:2234px){#D:before{content:"2234";}} @media (min-width:2235px){#D:before{content:"2235";}} @media (min-width:2236px){#D:before{content:"2236";}} @media (min-width:2237px){#D:before{content:"2237";}} @media (min-width:2238px){#D:before{content:"2238";}} @media (min-width:2239px){#D:before{content:"2239";}} @media (min-width:2240px){#D:before{content:"2240";}} @media (min-width:2241px){#D:before{content:"2241";}} @media (min-width:2242px){#D:before{content:"2242";}} @media (min-width:2243px){#D:before{content:"2243";}} @media (min-width:2244px){#D:before{content:"2244";}} @media (min-width:2245px){#D:before{content:"2245";}} @media (min-width:2246px){#D:before{content:"2246";}} @media (min-width:2247px){#D:before{content:"2247";}} @media (min-width:2248px){#D:before{content:"2248";}} @media (min-width:2249px){#D:before{content:"2249";}} @media (min-width:2250px){#D:before{content:"2250";}} @media (min-width:2251px){#D:before{content:"2251";}} @media (min-width:2252px){#D:before{content:"2252";}} @media (min-width:2253px){#D:before{content:"2253";}} @media (min-width:2254px){#D:before{content:"2254";}} @media (min-width:2255px){#D:before{content:"2255";}} @media (min-width:2256px){#D:before{content:"2256";}} @media (min-width:2257px){#D:before{content:"2257";}} @media (min-width:2258px){#D:before{content:"2258";}} @media (min-width:2259px){#D:before{content:"2259";}} @media (min-width:2260px){#D:before{content:"2260";}} @media (min-width:2261px){#D:before{content:"2261";}} @media (min-width:2262px){#D:before{content:"2262";}} @media (min-width:2263px){#D:before{content:"2263";}} @media (min-width:2264px){#D:before{content:"2264";}} @media (min-width:2265px){#D:before{content:"2265";}} @media (min-width:2266px){#D:before{content:"2266";}} @media (min-width:2267px){#D:before{content:"2267";}} @media (min-width:2268px){#D:before{content:"2268";}} @media (min-width:2269px){#D:before{content:"2269";}} @media (min-width:2270px){#D:before{content:"2270";}} @media (min-width:2271px){#D:before{content:"2271";}} @media (min-width:2272px){#D:before{content:"2272";}} @media (min-width:2273px){#D:before{content:"2273";}} @media (min-width:2274px){#D:before{content:"2274";}} @media (min-width:2275px){#D:before{content:"2275";}} @media (min-width:2276px){#D:before{content:"2276";}} @media (min-width:2277px){#D:before{content:"2277";}} @media (min-width:2278px){#D:before{content:"2278";}} @media (min-width:2279px){#D:before{content:"2279";}} @media (min-width:2280px){#D:before{content:"2280";}} @media (min-width:2281px){#D:before{content:"2281";}} @media (min-width:2282px){#D:before{content:"2282";}} @media (min-width:2283px){#D:before{content:"2283";}} @media (min-width:2284px){#D:before{content:"2284";}} @media (min-width:2285px){#D:before{content:"2285";}} @media (min-width:2286px){#D:before{content:"2286";}} @media (min-width:2287px){#D:before{content:"2287";}} @media (min-width:2288px){#D:before{content:"2288";}} @media (min-width:2289px){#D:before{content:"2289";}} @media (min-width:2290px){#D:before{content:"2290";}} @media (min-width:2291px){#D:before{content:"2291";}} @media (min-width:2292px){#D:before{content:"2292";}} @media (min-width:2293px){#D:before{content:"2293";}} @media (min-width:2294px){#D:before{content:"2294";}} @media (min-width:2295px){#D:before{content:"2295";}} @media (min-width:2296px){#D:before{content:"2296";}} @media (min-width:2297px){#D:before{content:"2297";}} @media (min-width:2298px){#D:before{content:"2298";}} @media (min-width:2299px){#D:before{content:"2299";}} @media (min-width:2300px){#D:before{content:"2300";}} @media (min-width:2301px){#D:before{content:"2301";}} @media (min-width:2302px){#D:before{content:"2302";}} @media (min-width:2303px){#D:before{content:"2303";}} @media (min-width:2304px){#D:before{content:"2304";}} @media (min-width:2305px){#D:before{content:"2305";}} @media (min-width:2306px){#D:before{content:"2306";}} @media (min-width:2307px){#D:before{content:"2307";}} @media (min-width:2308px){#D:before{content:"2308";}} @media (min-width:2309px){#D:before{content:"2309";}} @media (min-width:2310px){#D:before{content:"2310";}} @media (min-width:2311px){#D:before{content:"2311";}} @media (min-width:2312px){#D:before{content:"2312";}} @media (min-width:2313px){#D:before{content:"2313";}} @media (min-width:2314px){#D:before{content:"2314";}} @media (min-width:2315px){#D:before{content:"2315";}} @media (min-width:2316px){#D:before{content:"2316";}} @media (min-width:2317px){#D:before{content:"2317";}} @media (min-width:2318px){#D:before{content:"2318";}} @media (min-width:2319px){#D:before{content:"2319";}} @media (min-width:2320px){#D:before{content:"2320";}} @media (min-width:2321px){#D:before{content:"2321";}} @media (min-width:2322px){#D:before{content:"2322";}} @media (min-width:2323px){#D:before{content:"2323";}} @media (min-width:2324px){#D:before{content:"2324";}} @media (min-width:2325px){#D:before{content:"2325";}} @media (min-width:2326px){#D:before{content:"2326";}} @media (min-width:2327px){#D:before{content:"2327";}} @media (min-width:2328px){#D:before{content:"2328";}} @media (min-width:2329px){#D:before{content:"2329";}} @media (min-width:2330px){#D:before{content:"2330";}} @media (min-width:2331px){#D:before{content:"2331";}} @media (min-width:2332px){#D:before{content:"2332";}} @media (min-width:2333px){#D:before{content:"2333";}} @media (min-width:2334px){#D:before{content:"2334";}} @media (min-width:2335px){#D:before{content:"2335";}} @media (min-width:2336px){#D:before{content:"2336";}} @media (min-width:2337px){#D:before{content:"2337";}} @media (min-width:2338px){#D:before{content:"2338";}} @media (min-width:2339px){#D:before{content:"2339";}} @media (min-width:2340px){#D:before{content:"2340";}} @media (min-width:2341px){#D:before{content:"2341";}} @media (min-width:2342px){#D:before{content:"2342";}} @media (min-width:2343px){#D:before{content:"2343";}} @media (min-width:2344px){#D:before{content:"2344";}} @media (min-width:2345px){#D:before{content:"2345";}} @media (min-width:2346px){#D:before{content:"2346";}} @media (min-width:2347px){#D:before{content:"2347";}} @media (min-width:2348px){#D:before{content:"2348";}} @media (min-width:2349px){#D:before{content:"2349";}} @media (min-width:2350px){#D:before{content:"2350";}} @media (min-width:2351px){#D:before{content:"2351";}} @media (min-width:2352px){#D:before{content:"2352";}} @media (min-width:2353px){#D:before{content:"2353";}} @media (min-width:2354px){#D:before{content:"2354";}} @media (min-width:2355px){#D:before{content:"2355";}} @media (min-width:2356px){#D:before{content:"2356";}} @media (min-width:2357px){#D:before{content:"2357";}} @media (min-width:2358px){#D:before{content:"2358";}} @media (min-width:2359px){#D:before{content:"2359";}} @media (min-width:2360px){#D:before{content:"2360";}} @media (min-width:2361px){#D:before{content:"2361";}} @media (min-width:2362px){#D:before{content:"2362";}} @media (min-width:2363px){#D:before{content:"2363";}} @media (min-width:2364px){#D:before{content:"2364";}} @media (min-width:2365px){#D:before{content:"2365";}} @media (min-width:2366px){#D:before{content:"2366";}} @media (min-width:2367px){#D:before{content:"2367";}} @media (min-width:2368px){#D:before{content:"2368";}} @media (min-width:2369px){#D:before{content:"2369";}} @media (min-width:2370px){#D:before{content:"2370";}} @media (min-width:2371px){#D:before{content:"2371";}} @media (min-width:2372px){#D:before{content:"2372";}} @media (min-width:2373px){#D:before{content:"2373";}} @media (min-width:2374px){#D:before{content:"2374";}} @media (min-width:2375px){#D:before{content:"2375";}} @media (min-width:2376px){#D:before{content:"2376";}} @media (min-width:2377px){#D:before{content:"2377";}} @media (min-width:2378px){#D:before{content:"2378";}} @media (min-width:2379px){#D:before{content:"2379";}} @media (min-width:2380px){#D:before{content:"2380";}} @media (min-width:2381px){#D:before{content:"2381";}} @media (min-width:2382px){#D:before{content:"2382";}} @media (min-width:2383px){#D:before{content:"2383";}} @media (min-width:2384px){#D:before{content:"2384";}} @media (min-width:2385px){#D:before{content:"2385";}} @media (min-width:2386px){#D:before{content:"2386";}} @media (min-width:2387px){#D:before{content:"2387";}} @media (min-width:2388px){#D:before{content:"2388";}} @media (min-width:2389px){#D:before{content:"2389";}} @media (min-width:2390px){#D:before{content:"2390";}} @media (min-width:2391px){#D:before{content:"2391";}} @media (min-width:2392px){#D:before{content:"2392";}} @media (min-width:2393px){#D:before{content:"2393";}} @media (min-width:2394px){#D:before{content:"2394";}} @media (min-width:2395px){#D:before{content:"2395";}} @media (min-width:2396px){#D:before{content:"2396";}} @media (min-width:2397px){#D:before{content:"2397";}} @media (min-width:2398px){#D:before{content:"2398";}} @media (min-width:2399px){#D:before{content:"2399";}} @media (min-width:2400px){#D:before{content:"2400";}} @media (min-width:2401px){#D:before{content:"2401";}} @media (min-width:2402px){#D:before{content:"2402";}} @media (min-width:2403px){#D:before{content:"2403";}} @media (min-width:2404px){#D:before{content:"2404";}} @media (min-width:2405px){#D:before{content:"2405";}} @media (min-width:2406px){#D:before{content:"2406";}} @media (min-width:2407px){#D:before{content:"2407";}} @media (min-width:2408px){#D:before{content:"2408";}} @media (min-width:2409px){#D:before{content:"2409";}} @media (min-width:2410px){#D:before{content:"2410";}} @media (min-width:2411px){#D:before{content:"2411";}} @media (min-width:2412px){#D:before{content:"2412";}} @media (min-width:2413px){#D:before{content:"2413";}} @media (min-width:2414px){#D:before{content:"2414";}} @media (min-width:2415px){#D:before{content:"2415";}} @media (min-width:2416px){#D:before{content:"2416";}} @media (min-width:2417px){#D:before{content:"2417";}} @media (min-width:2418px){#D:before{content:"2418";}} @media (min-width:2419px){#D:before{content:"2419";}} @media (min-width:2420px){#D:before{content:"2420";}} @media (min-width:2421px){#D:before{content:"2421";}} @media (min-width:2422px){#D:before{content:"2422";}} @media (min-width:2423px){#D:before{content:"2423";}} @media (min-width:2424px){#D:before{content:"2424";}} @media (min-width:2425px){#D:before{content:"2425";}} @media (min-width:2426px){#D:before{content:"2426";}} @media (min-width:2427px){#D:before{content:"2427";}} @media (min-width:2428px){#D:before{content:"2428";}} @media (min-width:2429px){#D:before{content:"2429";}} @media (min-width:2430px){#D:before{content:"2430";}} @media (min-width:2431px){#D:before{content:"2431";}} @media (min-width:2432px){#D:before{content:"2432";}} @media (min-width:2433px){#D:before{content:"2433";}} @media (min-width:2434px){#D:before{content:"2434";}} @media (min-width:2435px){#D:before{content:"2435";}} @media (min-width:2436px){#D:before{content:"2436";}} @media (min-width:2437px){#D:before{content:"2437";}} @media (min-width:2438px){#D:before{content:"2438";}} @media (min-width:2439px){#D:before{content:"2439";}} @media (min-width:2440px){#D:before{content:"2440";}} @media (min-width:2441px){#D:before{content:"2441";}} @media (min-width:2442px){#D:before{content:"2442";}} @media (min-width:2443px){#D:before{content:"2443";}} @media (min-width:2444px){#D:before{content:"2444";}} @media (min-width:2445px){#D:before{content:"2445";}} @media (min-width:2446px){#D:before{content:"2446";}} @media (min-width:2447px){#D:before{content:"2447";}} @media (min-width:2448px){#D:before{content:"2448";}} @media (min-width:2449px){#D:before{content:"2449";}} @media (min-width:2450px){#D:before{content:"2450";}} @media (min-width:2451px){#D:before{content:"2451";}} @media (min-width:2452px){#D:before{content:"2452";}} @media (min-width:2453px){#D:before{content:"2453";}} @media (min-width:2454px){#D:before{content:"2454";}} @media (min-width:2455px){#D:before{content:"2455";}} @media (min-width:2456px){#D:before{content:"2456";}} @media (min-width:2457px){#D:before{content:"2457";}} @media (min-width:2458px){#D:before{content:"2458";}} @media (min-width:2459px){#D:before{content:"2459";}} @media (min-width:2460px){#D:before{content:"2460";}} @media (min-width:2461px){#D:before{content:"2461";}} @media (min-width:2462px){#D:before{content:"2462";}} @media (min-width:2463px){#D:before{content:"2463";}} @media (min-width:2464px){#D:before{content:"2464";}} @media (min-width:2465px){#D:before{content:"2465";}} @media (min-width:2466px){#D:before{content:"2466";}} @media (min-width:2467px){#D:before{content:"2467";}} @media (min-width:2468px){#D:before{content:"2468";}} @media (min-width:2469px){#D:before{content:"2469";}} @media (min-width:2470px){#D:before{content:"2470";}} @media (min-width:2471px){#D:before{content:"2471";}} @media (min-width:2472px){#D:before{content:"2472";}} @media (min-width:2473px){#D:before{content:"2473";}} @media (min-width:2474px){#D:before{content:"2474";}} @media (min-width:2475px){#D:before{content:"2475";}} @media (min-width:2476px){#D:before{content:"2476";}} @media (min-width:2477px){#D:before{content:"2477";}} @media (min-width:2478px){#D:before{content:"2478";}} @media (min-width:2479px){#D:before{content:"2479";}} @media (min-width:2480px){#D:before{content:"2480";}} @media (min-width:2481px){#D:before{content:"2481";}} @media (min-width:2482px){#D:before{content:"2482";}} @media (min-width:2483px){#D:before{content:"2483";}} @media (min-width:2484px){#D:before{content:"2484";}} @media (min-width:2485px){#D:before{content:"2485";}} @media (min-width:2486px){#D:before{content:"2486";}} @media (min-width:2487px){#D:before{content:"2487";}} @media (min-width:2488px){#D:before{content:"2488";}} @media (min-width:2489px){#D:before{content:"2489";}} @media (min-width:2490px){#D:before{content:"2490";}} @media (min-width:2491px){#D:before{content:"2491";}} @media (min-width:2492px){#D:before{content:"2492";}} @media (min-width:2493px){#D:before{content:"2493";}} @media (min-width:2494px){#D:before{content:"2494";}} @media (min-width:2495px){#D:before{content:"2495";}} @media (min-width:2496px){#D:before{content:"2496";}} @media (min-width:2497px){#D:before{content:"2497";}} @media (min-width:2498px){#D:before{content:"2498";}} @media (min-width:2499px){#D:before{content:"2499";}} @media (min-width:2500px){#D:before{content:"2500";}} @media (min-width:2501px){#D:before{content:"2501";}} @media (min-width:2502px){#D:before{content:"2502";}} @media (min-width:2503px){#D:before{content:"2503";}} @media (min-width:2504px){#D:before{content:"2504";}} @media (min-width:2505px){#D:before{content:"2505";}} @media (min-width:2506px){#D:before{content:"2506";}} @media (min-width:2507px){#D:before{content:"2507";}} @media (min-width:2508px){#D:before{content:"2508";}} @media (min-width:2509px){#D:before{content:"2509";}} @media (min-width:2510px){#D:before{content:"2510";}} @media (min-width:2511px){#D:before{content:"2511";}} @media (min-width:2512px){#D:before{content:"2512";}} @media (min-width:2513px){#D:before{content:"2513";}} @media (min-width:2514px){#D:before{content:"2514";}} @media (min-width:2515px){#D:before{content:"2515";}} @media (min-width:2516px){#D:before{content:"2516";}} @media (min-width:2517px){#D:before{content:"2517";}} @media (min-width:2518px){#D:before{content:"2518";}} @media (min-width:2519px){#D:before{content:"2519";}} @media (min-width:2520px){#D:before{content:"2520";}} @media (min-width:2521px){#D:before{content:"2521";}} @media (min-width:2522px){#D:before{content:"2522";}} @media (min-width:2523px){#D:before{content:"2523";}} @media (min-width:2524px){#D:before{content:"2524";}} @media (min-width:2525px){#D:before{content:"2525";}} @media (min-width:2526px){#D:before{content:"2526";}} @media (min-width:2527px){#D:before{content:"2527";}} @media (min-width:2528px){#D:before{content:"2528";}} @media (min-width:2529px){#D:before{content:"2529";}} @media (min-width:2530px){#D:before{content:"2530";}} @media (min-width:2531px){#D:before{content:"2531";}} @media (min-width:2532px){#D:before{content:"2532";}} @media (min-width:2533px){#D:before{content:"2533";}} @media (min-width:2534px){#D:before{content:"2534";}} @media (min-width:2535px){#D:before{content:"2535";}} @media (min-width:2536px){#D:before{content:"2536";}} @media (min-width:2537px){#D:before{content:"2537";}} @media (min-width:2538px){#D:before{content:"2538";}} @media (min-width:2539px){#D:before{content:"2539";}} @media (min-width:2540px){#D:before{content:"2540";}} @media (min-width:2541px){#D:before{content:"2541";}} @media (min-width:2542px){#D:before{content:"2542";}} @media (min-width:2543px){#D:before{content:"2543";}} @media (min-width:2544px){#D:before{content:"2544";}} @media (min-width:2545px){#D:before{content:"2545";}} @media (min-width:2546px){#D:before{content:"2546";}} @media (min-width:2547px){#D:before{content:"2547";}} @media (min-width:2548px){#D:before{content:"2548";}} @media (min-width:2549px){#D:before{content:"2549";}} @media (min-width:2550px){#D:before{content:"2550";}} @media (min-width:2551px){#D:before{content:"2551";}} @media (min-width:2552px){#D:before{content:"2552";}} @media (min-width:2553px){#D:before{content:"2553";}} @media (min-width:2554px){#D:before{content:"2554";}} @media (min-width:2555px){#D:before{content:"2555";}} @media (min-width:2556px){#D:before{content:"2556";}} @media (min-width:2557px){#D:before{content:"2557";}} @media (min-width:2558px){#D:before{content:"2558";}} @media (min-width:2559px){#D:before{content:"2559";}} @media (min-width:2560px){#D:before{content:"2560";}} @media (min-width:2561px){#D:before{content:"";}} /* upper */ @media (min-height:399px){#D:after{content:"";}} /* lower */ @media (min-height:400px){#D:after{content:" x 400";}} @media (min-height:401px){#D:after{content:" x 401";}} @media (min-height:402px){#D:after{content:" x 402";}} @media (min-height:403px){#D:after{content:" x 403";}} @media (min-height:404px){#D:after{content:" x 404";}} @media (min-height:405px){#D:after{content:" x 405";}} @media (min-height:406px){#D:after{content:" x 406";}} @media (min-height:407px){#D:after{content:" x 407";}} @media (min-height:408px){#D:after{content:" x 408";}} @media (min-height:409px){#D:after{content:" x 409";}} @media (min-height:410px){#D:after{content:" x 410";}} @media (min-height:411px){#D:after{content:" x 411";}} @media (min-height:412px){#D:after{content:" x 412";}} @media (min-height:413px){#D:after{content:" x 413";}} @media (min-height:414px){#D:after{content:" x 414";}} @media (min-height:415px){#D:after{content:" x 415";}} @media (min-height:416px){#D:after{content:" x 416";}} @media (min-height:417px){#D:after{content:" x 417";}} @media (min-height:418px){#D:after{content:" x 418";}} @media (min-height:419px){#D:after{content:" x 419";}} @media (min-height:420px){#D:after{content:" x 420";}} @media (min-height:421px){#D:after{content:" x 421";}} @media (min-height:422px){#D:after{content:" x 422";}} @media (min-height:423px){#D:after{content:" x 423";}} @media (min-height:424px){#D:after{content:" x 424";}} @media (min-height:425px){#D:after{content:" x 425";}} @media (min-height:426px){#D:after{content:" x 426";}} @media (min-height:427px){#D:after{content:" x 427";}} @media (min-height:428px){#D:after{content:" x 428";}} @media (min-height:429px){#D:after{content:" x 429";}} @media (min-height:430px){#D:after{content:" x 430";}} @media (min-height:431px){#D:after{content:" x 431";}} @media (min-height:432px){#D:after{content:" x 432";}} @media (min-height:433px){#D:after{content:" x 433";}} @media (min-height:434px){#D:after{content:" x 434";}} @media (min-height:435px){#D:after{content:" x 435";}} @media (min-height:436px){#D:after{content:" x 436";}} @media (min-height:437px){#D:after{content:" x 437";}} @media (min-height:438px){#D:after{content:" x 438";}} @media (min-height:439px){#D:after{content:" x 439";}} @media (min-height:440px){#D:after{content:" x 440";}} @media (min-height:441px){#D:after{content:" x 441";}} @media (min-height:442px){#D:after{content:" x 442";}} @media (min-height:443px){#D:after{content:" x 443";}} @media (min-height:444px){#D:after{content:" x 444";}} @media (min-height:445px){#D:after{content:" x 445";}} @media (min-height:446px){#D:after{content:" x 446";}} @media (min-height:447px){#D:after{content:" x 447";}} @media (min-height:448px){#D:after{content:" x 448";}} @media (min-height:449px){#D:after{content:" x 449";}} @media (min-height:450px){#D:after{content:" x 450";}} @media (min-height:451px){#D:after{content:" x 451";}} @media (min-height:452px){#D:after{content:" x 452";}} @media (min-height:453px){#D:after{content:" x 453";}} @media (min-height:454px){#D:after{content:" x 454";}} @media (min-height:455px){#D:after{content:" x 455";}} @media (min-height:456px){#D:after{content:" x 456";}} @media (min-height:457px){#D:after{content:" x 457";}} @media (min-height:458px){#D:after{content:" x 458";}} @media (min-height:459px){#D:after{content:" x 459";}} @media (min-height:460px){#D:after{content:" x 460";}} @media (min-height:461px){#D:after{content:" x 461";}} @media (min-height:462px){#D:after{content:" x 462";}} @media (min-height:463px){#D:after{content:" x 463";}} @media (min-height:464px){#D:after{content:" x 464";}} @media (min-height:465px){#D:after{content:" x 465";}} @media (min-height:466px){#D:after{content:" x 466";}} @media (min-height:467px){#D:after{content:" x 467";}} @media (min-height:468px){#D:after{content:" x 468";}} @media (min-height:469px){#D:after{content:" x 469";}} @media (min-height:470px){#D:after{content:" x 470";}} @media (min-height:471px){#D:after{content:" x 471";}} @media (min-height:472px){#D:after{content:" x 472";}} @media (min-height:473px){#D:after{content:" x 473";}} @media (min-height:474px){#D:after{content:" x 474";}} @media (min-height:475px){#D:after{content:" x 475";}} @media (min-height:476px){#D:after{content:" x 476";}} @media (min-height:477px){#D:after{content:" x 477";}} @media (min-height:478px){#D:after{content:" x 478";}} @media (min-height:479px){#D:after{content:" x 479";}} @media (min-height:480px){#D:after{content:" x 480";}} @media (min-height:481px){#D:after{content:" x 481";}} @media (min-height:482px){#D:after{content:" x 482";}} @media (min-height:483px){#D:after{content:" x 483";}} @media (min-height:484px){#D:after{content:" x 484";}} @media (min-height:485px){#D:after{content:" x 485";}} @media (min-height:486px){#D:after{content:" x 486";}} @media (min-height:487px){#D:after{content:" x 487";}} @media (min-height:488px){#D:after{content:" x 488";}} @media (min-height:489px){#D:after{content:" x 489";}} @media (min-height:490px){#D:after{content:" x 490";}} @media (min-height:491px){#D:after{content:" x 491";}} @media (min-height:492px){#D:after{content:" x 492";}} @media (min-height:493px){#D:after{content:" x 493";}} @media (min-height:494px){#D:after{content:" x 494";}} @media (min-height:495px){#D:after{content:" x 495";}} @media (min-height:496px){#D:after{content:" x 496";}} @media (min-height:497px){#D:after{content:" x 497";}} @media (min-height:498px){#D:after{content:" x 498";}} @media (min-height:499px){#D:after{content:" x 499";}} @media (min-height:500px){#D:after{content:" x 500";}} @media (min-height:501px){#D:after{content:" x 501";}} @media (min-height:502px){#D:after{content:" x 502";}} @media (min-height:503px){#D:after{content:" x 503";}} @media (min-height:504px){#D:after{content:" x 504";}} @media (min-height:505px){#D:after{content:" x 505";}} @media (min-height:506px){#D:after{content:" x 506";}} @media (min-height:507px){#D:after{content:" x 507";}} @media (min-height:508px){#D:after{content:" x 508";}} @media (min-height:509px){#D:after{content:" x 509";}} @media (min-height:510px){#D:after{content:" x 510";}} @media (min-height:511px){#D:after{content:" x 511";}} @media (min-height:512px){#D:after{content:" x 512";}} @media (min-height:513px){#D:after{content:" x 513";}} @media (min-height:514px){#D:after{content:" x 514";}} @media (min-height:515px){#D:after{content:" x 515";}} @media (min-height:516px){#D:after{content:" x 516";}} @media (min-height:517px){#D:after{content:" x 517";}} @media (min-height:518px){#D:after{content:" x 518";}} @media (min-height:519px){#D:after{content:" x 519";}} @media (min-height:520px){#D:after{content:" x 520";}} @media (min-height:521px){#D:after{content:" x 521";}} @media (min-height:522px){#D:after{content:" x 522";}} @media (min-height:523px){#D:after{content:" x 523";}} @media (min-height:524px){#D:after{content:" x 524";}} @media (min-height:525px){#D:after{content:" x 525";}} @media (min-height:526px){#D:after{content:" x 526";}} @media (min-height:527px){#D:after{content:" x 527";}} @media (min-height:528px){#D:after{content:" x 528";}} @media (min-height:529px){#D:after{content:" x 529";}} @media (min-height:530px){#D:after{content:" x 530";}} @media (min-height:531px){#D:after{content:" x 531";}} @media (min-height:532px){#D:after{content:" x 532";}} @media (min-height:533px){#D:after{content:" x 533";}} @media (min-height:534px){#D:after{content:" x 534";}} @media (min-height:535px){#D:after{content:" x 535";}} @media (min-height:536px){#D:after{content:" x 536";}} @media (min-height:537px){#D:after{content:" x 537";}} @media (min-height:538px){#D:after{content:" x 538";}} @media (min-height:539px){#D:after{content:" x 539";}} @media (min-height:540px){#D:after{content:" x 540";}} @media (min-height:541px){#D:after{content:" x 541";}} @media (min-height:542px){#D:after{content:" x 542";}} @media (min-height:543px){#D:after{content:" x 543";}} @media (min-height:544px){#D:after{content:" x 544";}} @media (min-height:545px){#D:after{content:" x 545";}} @media (min-height:546px){#D:after{content:" x 546";}} @media (min-height:547px){#D:after{content:" x 547";}} @media (min-height:548px){#D:after{content:" x 548";}} @media (min-height:549px){#D:after{content:" x 549";}} @media (min-height:550px){#D:after{content:" x 550";}} @media (min-height:551px){#D:after{content:" x 551";}} @media (min-height:552px){#D:after{content:" x 552";}} @media (min-height:553px){#D:after{content:" x 553";}} @media (min-height:554px){#D:after{content:" x 554";}} @media (min-height:555px){#D:after{content:" x 555";}} @media (min-height:556px){#D:after{content:" x 556";}} @media (min-height:557px){#D:after{content:" x 557";}} @media (min-height:558px){#D:after{content:" x 558";}} @media (min-height:559px){#D:after{content:" x 559";}} @media (min-height:560px){#D:after{content:" x 560";}} @media (min-height:561px){#D:after{content:" x 561";}} @media (min-height:562px){#D:after{content:" x 562";}} @media (min-height:563px){#D:after{content:" x 563";}} @media (min-height:564px){#D:after{content:" x 564";}} @media (min-height:565px){#D:after{content:" x 565";}} @media (min-height:566px){#D:after{content:" x 566";}} @media (min-height:567px){#D:after{content:" x 567";}} @media (min-height:568px){#D:after{content:" x 568";}} @media (min-height:569px){#D:after{content:" x 569";}} @media (min-height:570px){#D:after{content:" x 570";}} @media (min-height:571px){#D:after{content:" x 571";}} @media (min-height:572px){#D:after{content:" x 572";}} @media (min-height:573px){#D:after{content:" x 573";}} @media (min-height:574px){#D:after{content:" x 574";}} @media (min-height:575px){#D:after{content:" x 575";}} @media (min-height:576px){#D:after{content:" x 576";}} @media (min-height:577px){#D:after{content:" x 577";}} @media (min-height:578px){#D:after{content:" x 578";}} @media (min-height:579px){#D:after{content:" x 579";}} @media (min-height:580px){#D:after{content:" x 580";}} @media (min-height:581px){#D:after{content:" x 581";}} @media (min-height:582px){#D:after{content:" x 582";}} @media (min-height:583px){#D:after{content:" x 583";}} @media (min-height:584px){#D:after{content:" x 584";}} @media (min-height:585px){#D:after{content:" x 585";}} @media (min-height:586px){#D:after{content:" x 586";}} @media (min-height:587px){#D:after{content:" x 587";}} @media (min-height:588px){#D:after{content:" x 588";}} @media (min-height:589px){#D:after{content:" x 589";}} @media (min-height:590px){#D:after{content:" x 590";}} @media (min-height:591px){#D:after{content:" x 591";}} @media (min-height:592px){#D:after{content:" x 592";}} @media (min-height:593px){#D:after{content:" x 593";}} @media (min-height:594px){#D:after{content:" x 594";}} @media (min-height:595px){#D:after{content:" x 595";}} @media (min-height:596px){#D:after{content:" x 596";}} @media (min-height:597px){#D:after{content:" x 597";}} @media (min-height:598px){#D:after{content:" x 598";}} @media (min-height:599px){#D:after{content:" x 599";}} @media (min-height:600px){#D:after{content:" x 600";}} @media (min-height:601px){#D:after{content:" x 601";}} @media (min-height:602px){#D:after{content:" x 602";}} @media (min-height:603px){#D:after{content:" x 603";}} @media (min-height:604px){#D:after{content:" x 604";}} @media (min-height:605px){#D:after{content:" x 605";}} @media (min-height:606px){#D:after{content:" x 606";}} @media (min-height:607px){#D:after{content:" x 607";}} @media (min-height:608px){#D:after{content:" x 608";}} @media (min-height:609px){#D:after{content:" x 609";}} @media (min-height:610px){#D:after{content:" x 610";}} @media (min-height:611px){#D:after{content:" x 611";}} @media (min-height:612px){#D:after{content:" x 612";}} @media (min-height:613px){#D:after{content:" x 613";}} @media (min-height:614px){#D:after{content:" x 614";}} @media (min-height:615px){#D:after{content:" x 615";}} @media (min-height:616px){#D:after{content:" x 616";}} @media (min-height:617px){#D:after{content:" x 617";}} @media (min-height:618px){#D:after{content:" x 618";}} @media (min-height:619px){#D:after{content:" x 619";}} @media (min-height:620px){#D:after{content:" x 620";}} @media (min-height:621px){#D:after{content:" x 621";}} @media (min-height:622px){#D:after{content:" x 622";}} @media (min-height:623px){#D:after{content:" x 623";}} @media (min-height:624px){#D:after{content:" x 624";}} @media (min-height:625px){#D:after{content:" x 625";}} @media (min-height:626px){#D:after{content:" x 626";}} @media (min-height:627px){#D:after{content:" x 627";}} @media (min-height:628px){#D:after{content:" x 628";}} @media (min-height:629px){#D:after{content:" x 629";}} @media (min-height:630px){#D:after{content:" x 630";}} @media (min-height:631px){#D:after{content:" x 631";}} @media (min-height:632px){#D:after{content:" x 632";}} @media (min-height:633px){#D:after{content:" x 633";}} @media (min-height:634px){#D:after{content:" x 634";}} @media (min-height:635px){#D:after{content:" x 635";}} @media (min-height:636px){#D:after{content:" x 636";}} @media (min-height:637px){#D:after{content:" x 637";}} @media (min-height:638px){#D:after{content:" x 638";}} @media (min-height:639px){#D:after{content:" x 639";}} @media (min-height:640px){#D:after{content:" x 640";}} @media (min-height:641px){#D:after{content:" x 641";}} @media (min-height:642px){#D:after{content:" x 642";}} @media (min-height:643px){#D:after{content:" x 643";}} @media (min-height:644px){#D:after{content:" x 644";}} @media (min-height:645px){#D:after{content:" x 645";}} @media (min-height:646px){#D:after{content:" x 646";}} @media (min-height:647px){#D:after{content:" x 647";}} @media (min-height:648px){#D:after{content:" x 648";}} @media (min-height:649px){#D:after{content:" x 649";}} @media (min-height:650px){#D:after{content:" x 650";}} @media (min-height:651px){#D:after{content:" x 651";}} @media (min-height:652px){#D:after{content:" x 652";}} @media (min-height:653px){#D:after{content:" x 653";}} @media (min-height:654px){#D:after{content:" x 654";}} @media (min-height:655px){#D:after{content:" x 655";}} @media (min-height:656px){#D:after{content:" x 656";}} @media (min-height:657px){#D:after{content:" x 657";}} @media (min-height:658px){#D:after{content:" x 658";}} @media (min-height:659px){#D:after{content:" x 659";}} @media (min-height:660px){#D:after{content:" x 660";}} @media (min-height:661px){#D:after{content:" x 661";}} @media (min-height:662px){#D:after{content:" x 662";}} @media (min-height:663px){#D:after{content:" x 663";}} @media (min-height:664px){#D:after{content:" x 664";}} @media (min-height:665px){#D:after{content:" x 665";}} @media (min-height:666px){#D:after{content:" x 666";}} @media (min-height:667px){#D:after{content:" x 667";}} @media (min-height:668px){#D:after{content:" x 668";}} @media (min-height:669px){#D:after{content:" x 669";}} @media (min-height:670px){#D:after{content:" x 670";}} @media (min-height:671px){#D:after{content:" x 671";}} @media (min-height:672px){#D:after{content:" x 672";}} @media (min-height:673px){#D:after{content:" x 673";}} @media (min-height:674px){#D:after{content:" x 674";}} @media (min-height:675px){#D:after{content:" x 675";}} @media (min-height:676px){#D:after{content:" x 676";}} @media (min-height:677px){#D:after{content:" x 677";}} @media (min-height:678px){#D:after{content:" x 678";}} @media (min-height:679px){#D:after{content:" x 679";}} @media (min-height:680px){#D:after{content:" x 680";}} @media (min-height:681px){#D:after{content:" x 681";}} @media (min-height:682px){#D:after{content:" x 682";}} @media (min-height:683px){#D:after{content:" x 683";}} @media (min-height:684px){#D:after{content:" x 684";}} @media (min-height:685px){#D:after{content:" x 685";}} @media (min-height:686px){#D:after{content:" x 686";}} @media (min-height:687px){#D:after{content:" x 687";}} @media (min-height:688px){#D:after{content:" x 688";}} @media (min-height:689px){#D:after{content:" x 689";}} @media (min-height:690px){#D:after{content:" x 690";}} @media (min-height:691px){#D:after{content:" x 691";}} @media (min-height:692px){#D:after{content:" x 692";}} @media (min-height:693px){#D:after{content:" x 693";}} @media (min-height:694px){#D:after{content:" x 694";}} @media (min-height:695px){#D:after{content:" x 695";}} @media (min-height:696px){#D:after{content:" x 696";}} @media (min-height:697px){#D:after{content:" x 697";}} @media (min-height:698px){#D:after{content:" x 698";}} @media (min-height:699px){#D:after{content:" x 699";}} @media (min-height:700px){#D:after{content:" x 700";}} @media (min-height:701px){#D:after{content:" x 701";}} @media (min-height:702px){#D:after{content:" x 702";}} @media (min-height:703px){#D:after{content:" x 703";}} @media (min-height:704px){#D:after{content:" x 704";}} @media (min-height:705px){#D:after{content:" x 705";}} @media (min-height:706px){#D:after{content:" x 706";}} @media (min-height:707px){#D:after{content:" x 707";}} @media (min-height:708px){#D:after{content:" x 708";}} @media (min-height:709px){#D:after{content:" x 709";}} @media (min-height:710px){#D:after{content:" x 710";}} @media (min-height:711px){#D:after{content:" x 711";}} @media (min-height:712px){#D:after{content:" x 712";}} @media (min-height:713px){#D:after{content:" x 713";}} @media (min-height:714px){#D:after{content:" x 714";}} @media (min-height:715px){#D:after{content:" x 715";}} @media (min-height:716px){#D:after{content:" x 716";}} @media (min-height:717px){#D:after{content:" x 717";}} @media (min-height:718px){#D:after{content:" x 718";}} @media (min-height:719px){#D:after{content:" x 719";}} @media (min-height:720px){#D:after{content:" x 720";}} @media (min-height:721px){#D:after{content:" x 721";}} @media (min-height:722px){#D:after{content:" x 722";}} @media (min-height:723px){#D:after{content:" x 723";}} @media (min-height:724px){#D:after{content:" x 724";}} @media (min-height:725px){#D:after{content:" x 725";}} @media (min-height:726px){#D:after{content:" x 726";}} @media (min-height:727px){#D:after{content:" x 727";}} @media (min-height:728px){#D:after{content:" x 728";}} @media (min-height:729px){#D:after{content:" x 729";}} @media (min-height:730px){#D:after{content:" x 730";}} @media (min-height:731px){#D:after{content:" x 731";}} @media (min-height:732px){#D:after{content:" x 732";}} @media (min-height:733px){#D:after{content:" x 733";}} @media (min-height:734px){#D:after{content:" x 734";}} @media (min-height:735px){#D:after{content:" x 735";}} @media (min-height:736px){#D:after{content:" x 736";}} @media (min-height:737px){#D:after{content:" x 737";}} @media (min-height:738px){#D:after{content:" x 738";}} @media (min-height:739px){#D:after{content:" x 739";}} @media (min-height:740px){#D:after{content:" x 740";}} @media (min-height:741px){#D:after{content:" x 741";}} @media (min-height:742px){#D:after{content:" x 742";}} @media (min-height:743px){#D:after{content:" x 743";}} @media (min-height:744px){#D:after{content:" x 744";}} @media (min-height:745px){#D:after{content:" x 745";}} @media (min-height:746px){#D:after{content:" x 746";}} @media (min-height:747px){#D:after{content:" x 747";}} @media (min-height:748px){#D:after{content:" x 748";}} @media (min-height:749px){#D:after{content:" x 749";}} @media (min-height:750px){#D:after{content:" x 750";}} @media (min-height:751px){#D:after{content:" x 751";}} @media (min-height:752px){#D:after{content:" x 752";}} @media (min-height:753px){#D:after{content:" x 753";}} @media (min-height:754px){#D:after{content:" x 754";}} @media (min-height:755px){#D:after{content:" x 755";}} @media (min-height:756px){#D:after{content:" x 756";}} @media (min-height:757px){#D:after{content:" x 757";}} @media (min-height:758px){#D:after{content:" x 758";}} @media (min-height:759px){#D:after{content:" x 759";}} @media (min-height:760px){#D:after{content:" x 760";}} @media (min-height:761px){#D:after{content:" x 761";}} @media (min-height:762px){#D:after{content:" x 762";}} @media (min-height:763px){#D:after{content:" x 763";}} @media (min-height:764px){#D:after{content:" x 764";}} @media (min-height:765px){#D:after{content:" x 765";}} @media (min-height:766px){#D:after{content:" x 766";}} @media (min-height:767px){#D:after{content:" x 767";}} @media (min-height:768px){#D:after{content:" x 768";}} @media (min-height:769px){#D:after{content:" x 769";}} @media (min-height:770px){#D:after{content:" x 770";}} @media (min-height:771px){#D:after{content:" x 771";}} @media (min-height:772px){#D:after{content:" x 772";}} @media (min-height:773px){#D:after{content:" x 773";}} @media (min-height:774px){#D:after{content:" x 774";}} @media (min-height:775px){#D:after{content:" x 775";}} @media (min-height:776px){#D:after{content:" x 776";}} @media (min-height:777px){#D:after{content:" x 777";}} @media (min-height:778px){#D:after{content:" x 778";}} @media (min-height:779px){#D:after{content:" x 779";}} @media (min-height:780px){#D:after{content:" x 780";}} @media (min-height:781px){#D:after{content:" x 781";}} @media (min-height:782px){#D:after{content:" x 782";}} @media (min-height:783px){#D:after{content:" x 783";}} @media (min-height:784px){#D:after{content:" x 784";}} @media (min-height:785px){#D:after{content:" x 785";}} @media (min-height:786px){#D:after{content:" x 786";}} @media (min-height:787px){#D:after{content:" x 787";}} @media (min-height:788px){#D:after{content:" x 788";}} @media (min-height:789px){#D:after{content:" x 789";}} @media (min-height:790px){#D:after{content:" x 790";}} @media (min-height:791px){#D:after{content:" x 791";}} @media (min-height:792px){#D:after{content:" x 792";}} @media (min-height:793px){#D:after{content:" x 793";}} @media (min-height:794px){#D:after{content:" x 794";}} @media (min-height:795px){#D:after{content:" x 795";}} @media (min-height:796px){#D:after{content:" x 796";}} @media (min-height:797px){#D:after{content:" x 797";}} @media (min-height:798px){#D:after{content:" x 798";}} @media (min-height:799px){#D:after{content:" x 799";}} @media (min-height:800px){#D:after{content:" x 800";}} @media (min-height:801px){#D:after{content:" x 801";}} @media (min-height:802px){#D:after{content:" x 802";}} @media (min-height:803px){#D:after{content:" x 803";}} @media (min-height:804px){#D:after{content:" x 804";}} @media (min-height:805px){#D:after{content:" x 805";}} @media (min-height:806px){#D:after{content:" x 806";}} @media (min-height:807px){#D:after{content:" x 807";}} @media (min-height:808px){#D:after{content:" x 808";}} @media (min-height:809px){#D:after{content:" x 809";}} @media (min-height:810px){#D:after{content:" x 810";}} @media (min-height:811px){#D:after{content:" x 811";}} @media (min-height:812px){#D:after{content:" x 812";}} @media (min-height:813px){#D:after{content:" x 813";}} @media (min-height:814px){#D:after{content:" x 814";}} @media (min-height:815px){#D:after{content:" x 815";}} @media (min-height:816px){#D:after{content:" x 816";}} @media (min-height:817px){#D:after{content:" x 817";}} @media (min-height:818px){#D:after{content:" x 818";}} @media (min-height:819px){#D:after{content:" x 819";}} @media (min-height:820px){#D:after{content:" x 820";}} @media (min-height:821px){#D:after{content:" x 821";}} @media (min-height:822px){#D:after{content:" x 822";}} @media (min-height:823px){#D:after{content:" x 823";}} @media (min-height:824px){#D:after{content:" x 824";}} @media (min-height:825px){#D:after{content:" x 825";}} @media (min-height:826px){#D:after{content:" x 826";}} @media (min-height:827px){#D:after{content:" x 827";}} @media (min-height:828px){#D:after{content:" x 828";}} @media (min-height:829px){#D:after{content:" x 829";}} @media (min-height:830px){#D:after{content:" x 830";}} @media (min-height:831px){#D:after{content:" x 831";}} @media (min-height:832px){#D:after{content:" x 832";}} @media (min-height:833px){#D:after{content:" x 833";}} @media (min-height:834px){#D:after{content:" x 834";}} @media (min-height:835px){#D:after{content:" x 835";}} @media (min-height:836px){#D:after{content:" x 836";}} @media (min-height:837px){#D:after{content:" x 837";}} @media (min-height:838px){#D:after{content:" x 838";}} @media (min-height:839px){#D:after{content:" x 839";}} @media (min-height:840px){#D:after{content:" x 840";}} @media (min-height:841px){#D:after{content:" x 841";}} @media (min-height:842px){#D:after{content:" x 842";}} @media (min-height:843px){#D:after{content:" x 843";}} @media (min-height:844px){#D:after{content:" x 844";}} @media (min-height:845px){#D:after{content:" x 845";}} @media (min-height:846px){#D:after{content:" x 846";}} @media (min-height:847px){#D:after{content:" x 847";}} @media (min-height:848px){#D:after{content:" x 848";}} @media (min-height:849px){#D:after{content:" x 849";}} @media (min-height:850px){#D:after{content:" x 850";}} @media (min-height:851px){#D:after{content:" x 851";}} @media (min-height:852px){#D:after{content:" x 852";}} @media (min-height:853px){#D:after{content:" x 853";}} @media (min-height:854px){#D:after{content:" x 854";}} @media (min-height:855px){#D:after{content:" x 855";}} @media (min-height:856px){#D:after{content:" x 856";}} @media (min-height:857px){#D:after{content:" x 857";}} @media (min-height:858px){#D:after{content:" x 858";}} @media (min-height:859px){#D:after{content:" x 859";}} @media (min-height:860px){#D:after{content:" x 860";}} @media (min-height:861px){#D:after{content:" x 861";}} @media (min-height:862px){#D:after{content:" x 862";}} @media (min-height:863px){#D:after{content:" x 863";}} @media (min-height:864px){#D:after{content:" x 864";}} @media (min-height:865px){#D:after{content:" x 865";}} @media (min-height:866px){#D:after{content:" x 866";}} @media (min-height:867px){#D:after{content:" x 867";}} @media (min-height:868px){#D:after{content:" x 868";}} @media (min-height:869px){#D:after{content:" x 869";}} @media (min-height:870px){#D:after{content:" x 870";}} @media (min-height:871px){#D:after{content:" x 871";}} @media (min-height:872px){#D:after{content:" x 872";}} @media (min-height:873px){#D:after{content:" x 873";}} @media (min-height:874px){#D:after{content:" x 874";}} @media (min-height:875px){#D:after{content:" x 875";}} @media (min-height:876px){#D:after{content:" x 876";}} @media (min-height:877px){#D:after{content:" x 877";}} @media (min-height:878px){#D:after{content:" x 878";}} @media (min-height:879px){#D:after{content:" x 879";}} @media (min-height:880px){#D:after{content:" x 880";}} @media (min-height:881px){#D:after{content:" x 881";}} @media (min-height:882px){#D:after{content:" x 882";}} @media (min-height:883px){#D:after{content:" x 883";}} @media (min-height:884px){#D:after{content:" x 884";}} @media (min-height:885px){#D:after{content:" x 885";}} @media (min-height:886px){#D:after{content:" x 886";}} @media (min-height:887px){#D:after{content:" x 887";}} @media (min-height:888px){#D:after{content:" x 888";}} @media (min-height:889px){#D:after{content:" x 889";}} @media (min-height:890px){#D:after{content:" x 890";}} @media (min-height:891px){#D:after{content:" x 891";}} @media (min-height:892px){#D:after{content:" x 892";}} @media (min-height:893px){#D:after{content:" x 893";}} @media (min-height:894px){#D:after{content:" x 894";}} @media (min-height:895px){#D:after{content:" x 895";}} @media (min-height:896px){#D:after{content:" x 896";}} @media (min-height:897px){#D:after{content:" x 897";}} @media (min-height:898px){#D:after{content:" x 898";}} @media (min-height:899px){#D:after{content:" x 899";}} @media (min-height:900px){#D:after{content:" x 900";}} @media (min-height:901px){#D:after{content:" x 901";}} @media (min-height:902px){#D:after{content:" x 902";}} @media (min-height:903px){#D:after{content:" x 903";}} @media (min-height:904px){#D:after{content:" x 904";}} @media (min-height:905px){#D:after{content:" x 905";}} @media (min-height:906px){#D:after{content:" x 906";}} @media (min-height:907px){#D:after{content:" x 907";}} @media (min-height:908px){#D:after{content:" x 908";}} @media (min-height:909px){#D:after{content:" x 909";}} @media (min-height:910px){#D:after{content:" x 910";}} @media (min-height:911px){#D:after{content:" x 911";}} @media (min-height:912px){#D:after{content:" x 912";}} @media (min-height:913px){#D:after{content:" x 913";}} @media (min-height:914px){#D:after{content:" x 914";}} @media (min-height:915px){#D:after{content:" x 915";}} @media (min-height:916px){#D:after{content:" x 916";}} @media (min-height:917px){#D:after{content:" x 917";}} @media (min-height:918px){#D:after{content:" x 918";}} @media (min-height:919px){#D:after{content:" x 919";}} @media (min-height:920px){#D:after{content:" x 920";}} @media (min-height:921px){#D:after{content:" x 921";}} @media (min-height:922px){#D:after{content:" x 922";}} @media (min-height:923px){#D:after{content:" x 923";}} @media (min-height:924px){#D:after{content:" x 924";}} @media (min-height:925px){#D:after{content:" x 925";}} @media (min-height:926px){#D:after{content:" x 926";}} @media (min-height:927px){#D:after{content:" x 927";}} @media (min-height:928px){#D:after{content:" x 928";}} @media (min-height:929px){#D:after{content:" x 929";}} @media (min-height:930px){#D:after{content:" x 930";}} @media (min-height:931px){#D:after{content:" x 931";}} @media (min-height:932px){#D:after{content:" x 932";}} @media (min-height:933px){#D:after{content:" x 933";}} @media (min-height:934px){#D:after{content:" x 934";}} @media (min-height:935px){#D:after{content:" x 935";}} @media (min-height:936px){#D:after{content:" x 936";}} @media (min-height:937px){#D:after{content:" x 937";}} @media (min-height:938px){#D:after{content:" x 938";}} @media (min-height:939px){#D:after{content:" x 939";}} @media (min-height:940px){#D:after{content:" x 940";}} @media (min-height:941px){#D:after{content:" x 941";}} @media (min-height:942px){#D:after{content:" x 942";}} @media (min-height:943px){#D:after{content:" x 943";}} @media (min-height:944px){#D:after{content:" x 944";}} @media (min-height:945px){#D:after{content:" x 945";}} @media (min-height:946px){#D:after{content:" x 946";}} @media (min-height:947px){#D:after{content:" x 947";}} @media (min-height:948px){#D:after{content:" x 948";}} @media (min-height:949px){#D:after{content:" x 949";}} @media (min-height:950px){#D:after{content:" x 950";}} @media (min-height:951px){#D:after{content:" x 951";}} @media (min-height:952px){#D:after{content:" x 952";}} @media (min-height:953px){#D:after{content:" x 953";}} @media (min-height:954px){#D:after{content:" x 954";}} @media (min-height:955px){#D:after{content:" x 955";}} @media (min-height:956px){#D:after{content:" x 956";}} @media (min-height:957px){#D:after{content:" x 957";}} @media (min-height:958px){#D:after{content:" x 958";}} @media (min-height:959px){#D:after{content:" x 959";}} @media (min-height:960px){#D:after{content:" x 960";}} @media (min-height:961px){#D:after{content:" x 961";}} @media (min-height:962px){#D:after{content:" x 962";}} @media (min-height:963px){#D:after{content:" x 963";}} @media (min-height:964px){#D:after{content:" x 964";}} @media (min-height:965px){#D:after{content:" x 965";}} @media (min-height:966px){#D:after{content:" x 966";}} @media (min-height:967px){#D:after{content:" x 967";}} @media (min-height:968px){#D:after{content:" x 968";}} @media (min-height:969px){#D:after{content:" x 969";}} @media (min-height:970px){#D:after{content:" x 970";}} @media (min-height:971px){#D:after{content:" x 971";}} @media (min-height:972px){#D:after{content:" x 972";}} @media (min-height:973px){#D:after{content:" x 973";}} @media (min-height:974px){#D:after{content:" x 974";}} @media (min-height:975px){#D:after{content:" x 975";}} @media (min-height:976px){#D:after{content:" x 976";}} @media (min-height:977px){#D:after{content:" x 977";}} @media (min-height:978px){#D:after{content:" x 978";}} @media (min-height:979px){#D:after{content:" x 979";}} @media (min-height:980px){#D:after{content:" x 980";}} @media (min-height:981px){#D:after{content:" x 981";}} @media (min-height:982px){#D:after{content:" x 982";}} @media (min-height:983px){#D:after{content:" x 983";}} @media (min-height:984px){#D:after{content:" x 984";}} @media (min-height:985px){#D:after{content:" x 985";}} @media (min-height:986px){#D:after{content:" x 986";}} @media (min-height:987px){#D:after{content:" x 987";}} @media (min-height:988px){#D:after{content:" x 988";}} @media (min-height:989px){#D:after{content:" x 989";}} @media (min-height:990px){#D:after{content:" x 990";}} @media (min-height:991px){#D:after{content:" x 991";}} @media (min-height:992px){#D:after{content:" x 992";}} @media (min-height:993px){#D:after{content:" x 993";}} @media (min-height:994px){#D:after{content:" x 994";}} @media (min-height:995px){#D:after{content:" x 995";}} @media (min-height:996px){#D:after{content:" x 996";}} @media (min-height:997px){#D:after{content:" x 997";}} @media (min-height:998px){#D:after{content:" x 998";}} @media (min-height:999px){#D:after{content:" x 999";}} @media (min-height:1000px){#D:after{content:" x 1000";}} @media (min-height:1001px){#D:after{content:" x 1001";}} @media (min-height:1002px){#D:after{content:" x 1002";}} @media (min-height:1003px){#D:after{content:" x 1003";}} @media (min-height:1004px){#D:after{content:" x 1004";}} @media (min-height:1005px){#D:after{content:" x 1005";}} @media (min-height:1006px){#D:after{content:" x 1006";}} @media (min-height:1007px){#D:after{content:" x 1007";}} @media (min-height:1008px){#D:after{content:" x 1008";}} @media (min-height:1009px){#D:after{content:" x 1009";}} @media (min-height:1010px){#D:after{content:" x 1010";}} @media (min-height:1011px){#D:after{content:" x 1011";}} @media (min-height:1012px){#D:after{content:" x 1012";}} @media (min-height:1013px){#D:after{content:" x 1013";}} @media (min-height:1014px){#D:after{content:" x 1014";}} @media (min-height:1015px){#D:after{content:" x 1015";}} @media (min-height:1016px){#D:after{content:" x 1016";}} @media (min-height:1017px){#D:after{content:" x 1017";}} @media (min-height:1018px){#D:after{content:" x 1018";}} @media (min-height:1019px){#D:after{content:" x 1019";}} @media (min-height:1020px){#D:after{content:" x 1020";}} @media (min-height:1021px){#D:after{content:" x 1021";}} @media (min-height:1022px){#D:after{content:" x 1022";}} @media (min-height:1023px){#D:after{content:" x 1023";}} @media (min-height:1024px){#D:after{content:" x 1024";}} @media (min-height:1025px){#D:after{content:" x 1025";}} @media (min-height:1026px){#D:after{content:" x 1026";}} @media (min-height:1027px){#D:after{content:" x 1027";}} @media (min-height:1028px){#D:after{content:" x 1028";}} @media (min-height:1029px){#D:after{content:" x 1029";}} @media (min-height:1030px){#D:after{content:" x 1030";}} @media (min-height:1031px){#D:after{content:" x 1031";}} @media (min-height:1032px){#D:after{content:" x 1032";}} @media (min-height:1033px){#D:after{content:" x 1033";}} @media (min-height:1034px){#D:after{content:" x 1034";}} @media (min-height:1035px){#D:after{content:" x 1035";}} @media (min-height:1036px){#D:after{content:" x 1036";}} @media (min-height:1037px){#D:after{content:" x 1037";}} @media (min-height:1038px){#D:after{content:" x 1038";}} @media (min-height:1039px){#D:after{content:" x 1039";}} @media (min-height:1040px){#D:after{content:" x 1040";}} @media (min-height:1041px){#D:after{content:" x 1041";}} @media (min-height:1042px){#D:after{content:" x 1042";}} @media (min-height:1043px){#D:after{content:" x 1043";}} @media (min-height:1044px){#D:after{content:" x 1044";}} @media (min-height:1045px){#D:after{content:" x 1045";}} @media (min-height:1046px){#D:after{content:" x 1046";}} @media (min-height:1047px){#D:after{content:" x 1047";}} @media (min-height:1048px){#D:after{content:" x 1048";}} @media (min-height:1049px){#D:after{content:" x 1049";}} @media (min-height:1050px){#D:after{content:" x 1050";}} @media (min-height:1051px){#D:after{content:" x 1051";}} @media (min-height:1052px){#D:after{content:" x 1052";}} @media (min-height:1053px){#D:after{content:" x 1053";}} @media (min-height:1054px){#D:after{content:" x 1054";}} @media (min-height:1055px){#D:after{content:" x 1055";}} @media (min-height:1056px){#D:after{content:" x 1056";}} @media (min-height:1057px){#D:after{content:" x 1057";}} @media (min-height:1058px){#D:after{content:" x 1058";}} @media (min-height:1059px){#D:after{content:" x 1059";}} @media (min-height:1060px){#D:after{content:" x 1060";}} @media (min-height:1061px){#D:after{content:" x 1061";}} @media (min-height:1062px){#D:after{content:" x 1062";}} @media (min-height:1063px){#D:after{content:" x 1063";}} @media (min-height:1064px){#D:after{content:" x 1064";}} @media (min-height:1065px){#D:after{content:" x 1065";}} @media (min-height:1066px){#D:after{content:" x 1066";}} @media (min-height:1067px){#D:after{content:" x 1067";}} @media (min-height:1068px){#D:after{content:" x 1068";}} @media (min-height:1069px){#D:after{content:" x 1069";}} @media (min-height:1070px){#D:after{content:" x 1070";}} @media (min-height:1071px){#D:after{content:" x 1071";}} @media (min-height:1072px){#D:after{content:" x 1072";}} @media (min-height:1073px){#D:after{content:" x 1073";}} @media (min-height:1074px){#D:after{content:" x 1074";}} @media (min-height:1075px){#D:after{content:" x 1075";}} @media (min-height:1076px){#D:after{content:" x 1076";}} @media (min-height:1077px){#D:after{content:" x 1077";}} @media (min-height:1078px){#D:after{content:" x 1078";}} @media (min-height:1079px){#D:after{content:" x 1079";}} @media (min-height:1080px){#D:after{content:" x 1080";}} @media (min-height:1081px){#D:after{content:" x 1081";}} @media (min-height:1082px){#D:after{content:" x 1082";}} @media (min-height:1083px){#D:after{content:" x 1083";}} @media (min-height:1084px){#D:after{content:" x 1084";}} @media (min-height:1085px){#D:after{content:" x 1085";}} @media (min-height:1086px){#D:after{content:" x 1086";}} @media (min-height:1087px){#D:after{content:" x 1087";}} @media (min-height:1088px){#D:after{content:" x 1088";}} @media (min-height:1089px){#D:after{content:" x 1089";}} @media (min-height:1090px){#D:after{content:" x 1090";}} @media (min-height:1091px){#D:after{content:" x 1091";}} @media (min-height:1092px){#D:after{content:" x 1092";}} @media (min-height:1093px){#D:after{content:" x 1093";}} @media (min-height:1094px){#D:after{content:" x 1094";}} @media (min-height:1095px){#D:after{content:" x 1095";}} @media (min-height:1096px){#D:after{content:" x 1096";}} @media (min-height:1097px){#D:after{content:" x 1097";}} @media (min-height:1098px){#D:after{content:" x 1098";}} @media (min-height:1099px){#D:after{content:" x 1099";}} @media (min-height:1100px){#D:after{content:" x 1100";}} @media (min-height:1101px){#D:after{content:" x 1101";}} @media (min-height:1102px){#D:after{content:" x 1102";}} @media (min-height:1103px){#D:after{content:" x 1103";}} @media (min-height:1104px){#D:after{content:" x 1104";}} @media (min-height:1105px){#D:after{content:" x 1105";}} @media (min-height:1106px){#D:after{content:" x 1106";}} @media (min-height:1107px){#D:after{content:" x 1107";}} @media (min-height:1108px){#D:after{content:" x 1108";}} @media (min-height:1109px){#D:after{content:" x 1109";}} @media (min-height:1110px){#D:after{content:" x 1110";}} @media (min-height:1111px){#D:after{content:" x 1111";}} @media (min-height:1112px){#D:after{content:" x 1112";}} @media (min-height:1113px){#D:after{content:" x 1113";}} @media (min-height:1114px){#D:after{content:" x 1114";}} @media (min-height:1115px){#D:after{content:" x 1115";}} @media (min-height:1116px){#D:after{content:" x 1116";}} @media (min-height:1117px){#D:after{content:" x 1117";}} @media (min-height:1118px){#D:after{content:" x 1118";}} @media (min-height:1119px){#D:after{content:" x 1119";}} @media (min-height:1120px){#D:after{content:" x 1120";}} @media (min-height:1121px){#D:after{content:" x 1121";}} @media (min-height:1122px){#D:after{content:" x 1122";}} @media (min-height:1123px){#D:after{content:" x 1123";}} @media (min-height:1124px){#D:after{content:" x 1124";}} @media (min-height:1125px){#D:after{content:" x 1125";}} @media (min-height:1126px){#D:after{content:" x 1126";}} @media (min-height:1127px){#D:after{content:" x 1127";}} @media (min-height:1128px){#D:after{content:" x 1128";}} @media (min-height:1129px){#D:after{content:" x 1129";}} @media (min-height:1130px){#D:after{content:" x 1130";}} @media (min-height:1131px){#D:after{content:" x 1131";}} @media (min-height:1132px){#D:after{content:" x 1132";}} @media (min-height:1133px){#D:after{content:" x 1133";}} @media (min-height:1134px){#D:after{content:" x 1134";}} @media (min-height:1135px){#D:after{content:" x 1135";}} @media (min-height:1136px){#D:after{content:" x 1136";}} @media (min-height:1137px){#D:after{content:" x 1137";}} @media (min-height:1138px){#D:after{content:" x 1138";}} @media (min-height:1139px){#D:after{content:" x 1139";}} @media (min-height:1140px){#D:after{content:" x 1140";}} @media (min-height:1141px){#D:after{content:" x 1141";}} @media (min-height:1142px){#D:after{content:" x 1142";}} @media (min-height:1143px){#D:after{content:" x 1143";}} @media (min-height:1144px){#D:after{content:" x 1144";}} @media (min-height:1145px){#D:after{content:" x 1145";}} @media (min-height:1146px){#D:after{content:" x 1146";}} @media (min-height:1147px){#D:after{content:" x 1147";}} @media (min-height:1148px){#D:after{content:" x 1148";}} @media (min-height:1149px){#D:after{content:" x 1149";}} @media (min-height:1150px){#D:after{content:" x 1150";}} @media (min-height:1151px){#D:after{content:" x 1151";}} @media (min-height:1152px){#D:after{content:" x 1152";}} @media (min-height:1153px){#D:after{content:" x 1153";}} @media (min-height:1154px){#D:after{content:" x 1154";}} @media (min-height:1155px){#D:after{content:" x 1155";}} @media (min-height:1156px){#D:after{content:" x 1156";}} @media (min-height:1157px){#D:after{content:" x 1157";}} @media (min-height:1158px){#D:after{content:" x 1158";}} @media (min-height:1159px){#D:after{content:" x 1159";}} @media (min-height:1160px){#D:after{content:" x 1160";}} @media (min-height:1161px){#D:after{content:" x 1161";}} @media (min-height:1162px){#D:after{content:" x 1162";}} @media (min-height:1163px){#D:after{content:" x 1163";}} @media (min-height:1164px){#D:after{content:" x 1164";}} @media (min-height:1165px){#D:after{content:" x 1165";}} @media (min-height:1166px){#D:after{content:" x 1166";}} @media (min-height:1167px){#D:after{content:" x 1167";}} @media (min-height:1168px){#D:after{content:" x 1168";}} @media (min-height:1169px){#D:after{content:" x 1169";}} @media (min-height:1170px){#D:after{content:" x 1170";}} @media (min-height:1171px){#D:after{content:" x 1171";}} @media (min-height:1172px){#D:after{content:" x 1172";}} @media (min-height:1173px){#D:after{content:" x 1173";}} @media (min-height:1174px){#D:after{content:" x 1174";}} @media (min-height:1175px){#D:after{content:" x 1175";}} @media (min-height:1176px){#D:after{content:" x 1176";}} @media (min-height:1177px){#D:after{content:" x 1177";}} @media (min-height:1178px){#D:after{content:" x 1178";}} @media (min-height:1179px){#D:after{content:" x 1179";}} @media (min-height:1180px){#D:after{content:" x 1180";}} @media (min-height:1181px){#D:after{content:" x 1181";}} @media (min-height:1182px){#D:after{content:" x 1182";}} @media (min-height:1183px){#D:after{content:" x 1183";}} @media (min-height:1184px){#D:after{content:" x 1184";}} @media (min-height:1185px){#D:after{content:" x 1185";}} @media (min-height:1186px){#D:after{content:" x 1186";}} @media (min-height:1187px){#D:after{content:" x 1187";}} @media (min-height:1188px){#D:after{content:" x 1188";}} @media (min-height:1189px){#D:after{content:" x 1189";}} @media (min-height:1190px){#D:after{content:" x 1190";}} @media (min-height:1191px){#D:after{content:" x 1191";}} @media (min-height:1192px){#D:after{content:" x 1192";}} @media (min-height:1193px){#D:after{content:" x 1193";}} @media (min-height:1194px){#D:after{content:" x 1194";}} @media (min-height:1195px){#D:after{content:" x 1195";}} @media (min-height:1196px){#D:after{content:" x 1196";}} @media (min-height:1197px){#D:after{content:" x 1197";}} @media (min-height:1198px){#D:after{content:" x 1198";}} @media (min-height:1199px){#D:after{content:" x 1199";}} @media (min-height:1200px){#D:after{content:" x 1200";}} @media (min-height:1201px){#D:after{content:" x 1201";}} @media (min-height:1202px){#D:after{content:" x 1202";}} @media (min-height:1203px){#D:after{content:" x 1203";}} @media (min-height:1204px){#D:after{content:" x 1204";}} @media (min-height:1205px){#D:after{content:" x 1205";}} @media (min-height:1206px){#D:after{content:" x 1206";}} @media (min-height:1207px){#D:after{content:" x 1207";}} @media (min-height:1208px){#D:after{content:" x 1208";}} @media (min-height:1209px){#D:after{content:" x 1209";}} @media (min-height:1210px){#D:after{content:" x 1210";}} @media (min-height:1211px){#D:after{content:" x 1211";}} @media (min-height:1212px){#D:after{content:" x 1212";}} @media (min-height:1213px){#D:after{content:" x 1213";}} @media (min-height:1214px){#D:after{content:" x 1214";}} @media (min-height:1215px){#D:after{content:" x 1215";}} @media (min-height:1216px){#D:after{content:" x 1216";}} @media (min-height:1217px){#D:after{content:" x 1217";}} @media (min-height:1218px){#D:after{content:" x 1218";}} @media (min-height:1219px){#D:after{content:" x 1219";}} @media (min-height:1220px){#D:after{content:" x 1220";}} @media (min-height:1221px){#D:after{content:" x 1221";}} @media (min-height:1222px){#D:after{content:" x 1222";}} @media (min-height:1223px){#D:after{content:" x 1223";}} @media (min-height:1224px){#D:after{content:" x 1224";}} @media (min-height:1225px){#D:after{content:" x 1225";}} @media (min-height:1226px){#D:after{content:" x 1226";}} @media (min-height:1227px){#D:after{content:" x 1227";}} @media (min-height:1228px){#D:after{content:" x 1228";}} @media (min-height:1229px){#D:after{content:" x 1229";}} @media (min-height:1230px){#D:after{content:" x 1230";}} @media (min-height:1231px){#D:after{content:" x 1231";}} @media (min-height:1232px){#D:after{content:" x 1232";}} @media (min-height:1233px){#D:after{content:" x 1233";}} @media (min-height:1234px){#D:after{content:" x 1234";}} @media (min-height:1235px){#D:after{content:" x 1235";}} @media (min-height:1236px){#D:after{content:" x 1236";}} @media (min-height:1237px){#D:after{content:" x 1237";}} @media (min-height:1238px){#D:after{content:" x 1238";}} @media (min-height:1239px){#D:after{content:" x 1239";}} @media (min-height:1240px){#D:after{content:" x 1240";}} @media (min-height:1241px){#D:after{content:" x 1241";}} @media (min-height:1242px){#D:after{content:" x 1242";}} @media (min-height:1243px){#D:after{content:" x 1243";}} @media (min-height:1244px){#D:after{content:" x 1244";}} @media (min-height:1245px){#D:after{content:" x 1245";}} @media (min-height:1246px){#D:after{content:" x 1246";}} @media (min-height:1247px){#D:after{content:" x 1247";}} @media (min-height:1248px){#D:after{content:" x 1248";}} @media (min-height:1249px){#D:after{content:" x 1249";}} @media (min-height:1250px){#D:after{content:" x 1250";}} @media (min-height:1251px){#D:after{content:" x 1251";}} @media (min-height:1252px){#D:after{content:" x 1252";}} @media (min-height:1253px){#D:after{content:" x 1253";}} @media (min-height:1254px){#D:after{content:" x 1254";}} @media (min-height:1255px){#D:after{content:" x 1255";}} @media (min-height:1256px){#D:after{content:" x 1256";}} @media (min-height:1257px){#D:after{content:" x 1257";}} @media (min-height:1258px){#D:after{content:" x 1258";}} @media (min-height:1259px){#D:after{content:" x 1259";}} @media (min-height:1260px){#D:after{content:" x 1260";}} @media (min-height:1261px){#D:after{content:" x 1261";}} @media (min-height:1262px){#D:after{content:" x 1262";}} @media (min-height:1263px){#D:after{content:" x 1263";}} @media (min-height:1264px){#D:after{content:" x 1264";}} @media (min-height:1265px){#D:after{content:" x 1265";}} @media (min-height:1266px){#D:after{content:" x 1266";}} @media (min-height:1267px){#D:after{content:" x 1267";}} @media (min-height:1268px){#D:after{content:" x 1268";}} @media (min-height:1269px){#D:after{content:" x 1269";}} @media (min-height:1270px){#D:after{content:" x 1270";}} @media (min-height:1271px){#D:after{content:" x 1271";}} @media (min-height:1272px){#D:after{content:" x 1272";}} @media (min-height:1273px){#D:after{content:" x 1273";}} @media (min-height:1274px){#D:after{content:" x 1274";}} @media (min-height:1275px){#D:after{content:" x 1275";}} @media (min-height:1276px){#D:after{content:" x 1276";}} @media (min-height:1277px){#D:after{content:" x 1277";}} @media (min-height:1278px){#D:after{content:" x 1278";}} @media (min-height:1279px){#D:after{content:" x 1279";}} @media (min-height:1280px){#D:after{content:" x 1280";}} @media (min-height:1281px){#D:after{content:" x 1281";}} @media (min-height:1282px){#D:after{content:" x 1282";}} @media (min-height:1283px){#D:after{content:" x 1283";}} @media (min-height:1284px){#D:after{content:" x 1284";}} @media (min-height:1285px){#D:after{content:" x 1285";}} @media (min-height:1286px){#D:after{content:" x 1286";}} @media (min-height:1287px){#D:after{content:" x 1287";}} @media (min-height:1288px){#D:after{content:" x 1288";}} @media (min-height:1289px){#D:after{content:" x 1289";}} @media (min-height:1290px){#D:after{content:" x 1290";}} @media (min-height:1291px){#D:after{content:" x 1291";}} @media (min-height:1292px){#D:after{content:" x 1292";}} @media (min-height:1293px){#D:after{content:" x 1293";}} @media (min-height:1294px){#D:after{content:" x 1294";}} @media (min-height:1295px){#D:after{content:" x 1295";}} @media (min-height:1296px){#D:after{content:" x 1296";}} @media (min-height:1297px){#D:after{content:" x 1297";}} @media (min-height:1298px){#D:after{content:" x 1298";}} @media (min-height:1299px){#D:after{content:" x 1299";}} @media (min-height:1300px){#D:after{content:" x 1300";}} @media (min-height:1301px){#D:after{content:" x 1301";}} @media (min-height:1302px){#D:after{content:" x 1302";}} @media (min-height:1303px){#D:after{content:" x 1303";}} @media (min-height:1304px){#D:after{content:" x 1304";}} @media (min-height:1305px){#D:after{content:" x 1305";}} @media (min-height:1306px){#D:after{content:" x 1306";}} @media (min-height:1307px){#D:after{content:" x 1307";}} @media (min-height:1308px){#D:after{content:" x 1308";}} @media (min-height:1309px){#D:after{content:" x 1309";}} @media (min-height:1310px){#D:after{content:" x 1310";}} @media (min-height:1311px){#D:after{content:" x 1311";}} @media (min-height:1312px){#D:after{content:" x 1312";}} @media (min-height:1313px){#D:after{content:" x 1313";}} @media (min-height:1314px){#D:after{content:" x 1314";}} @media (min-height:1315px){#D:after{content:" x 1315";}} @media (min-height:1316px){#D:after{content:" x 1316";}} @media (min-height:1317px){#D:after{content:" x 1317";}} @media (min-height:1318px){#D:after{content:" x 1318";}} @media (min-height:1319px){#D:after{content:" x 1319";}} @media (min-height:1320px){#D:after{content:" x 1320";}} @media (min-height:1321px){#D:after{content:" x 1321";}} @media (min-height:1322px){#D:after{content:" x 1322";}} @media (min-height:1323px){#D:after{content:" x 1323";}} @media (min-height:1324px){#D:after{content:" x 1324";}} @media (min-height:1325px){#D:after{content:" x 1325";}} @media (min-height:1326px){#D:after{content:" x 1326";}} @media (min-height:1327px){#D:after{content:" x 1327";}} @media (min-height:1328px){#D:after{content:" x 1328";}} @media (min-height:1329px){#D:after{content:" x 1329";}} @media (min-height:1330px){#D:after{content:" x 1330";}} @media (min-height:1331px){#D:after{content:" x 1331";}} @media (min-height:1332px){#D:after{content:" x 1332";}} @media (min-height:1333px){#D:after{content:" x 1333";}} @media (min-height:1334px){#D:after{content:" x 1334";}} @media (min-height:1335px){#D:after{content:" x 1335";}} @media (min-height:1336px){#D:after{content:" x 1336";}} @media (min-height:1337px){#D:after{content:" x 1337";}} @media (min-height:1338px){#D:after{content:" x 1338";}} @media (min-height:1339px){#D:after{content:" x 1339";}} @media (min-height:1340px){#D:after{content:" x 1340";}} @media (min-height:1341px){#D:after{content:" x 1341";}} @media (min-height:1342px){#D:after{content:" x 1342";}} @media (min-height:1343px){#D:after{content:" x 1343";}} @media (min-height:1344px){#D:after{content:" x 1344";}} @media (min-height:1345px){#D:after{content:" x 1345";}} @media (min-height:1346px){#D:after{content:" x 1346";}} @media (min-height:1347px){#D:after{content:" x 1347";}} @media (min-height:1348px){#D:after{content:" x 1348";}} @media (min-height:1349px){#D:after{content:" x 1349";}} @media (min-height:1350px){#D:after{content:" x 1350";}} @media (min-height:1351px){#D:after{content:" x 1351";}} @media (min-height:1352px){#D:after{content:" x 1352";}} @media (min-height:1353px){#D:after{content:" x 1353";}} @media (min-height:1354px){#D:after{content:" x 1354";}} @media (min-height:1355px){#D:after{content:" x 1355";}} @media (min-height:1356px){#D:after{content:" x 1356";}} @media (min-height:1357px){#D:after{content:" x 1357";}} @media (min-height:1358px){#D:after{content:" x 1358";}} @media (min-height:1359px){#D:after{content:" x 1359";}} @media (min-height:1360px){#D:after{content:" x 1360";}} @media (min-height:1361px){#D:after{content:" x 1361";}} @media (min-height:1362px){#D:after{content:" x 1362";}} @media (min-height:1363px){#D:after{content:" x 1363";}} @media (min-height:1364px){#D:after{content:" x 1364";}} @media (min-height:1365px){#D:after{content:" x 1365";}} @media (min-height:1366px){#D:after{content:" x 1366";}} @media (min-height:1367px){#D:after{content:" x 1367";}} @media (min-height:1368px){#D:after{content:" x 1368";}} @media (min-height:1369px){#D:after{content:" x 1369";}} @media (min-height:1370px){#D:after{content:" x 1370";}} @media (min-height:1371px){#D:after{content:" x 1371";}} @media (min-height:1372px){#D:after{content:" x 1372";}} @media (min-height:1373px){#D:after{content:" x 1373";}} @media (min-height:1374px){#D:after{content:" x 1374";}} @media (min-height:1375px){#D:after{content:" x 1375";}} @media (min-height:1376px){#D:after{content:" x 1376";}} @media (min-height:1377px){#D:after{content:" x 1377";}} @media (min-height:1378px){#D:after{content:" x 1378";}} @media (min-height:1379px){#D:after{content:" x 1379";}} @media (min-height:1380px){#D:after{content:" x 1380";}} @media (min-height:1381px){#D:after{content:" x 1381";}} @media (min-height:1382px){#D:after{content:" x 1382";}} @media (min-height:1383px){#D:after{content:" x 1383";}} @media (min-height:1384px){#D:after{content:" x 1384";}} @media (min-height:1385px){#D:after{content:" x 1385";}} @media (min-height:1386px){#D:after{content:" x 1386";}} @media (min-height:1387px){#D:after{content:" x 1387";}} @media (min-height:1388px){#D:after{content:" x 1388";}} @media (min-height:1389px){#D:after{content:" x 1389";}} @media (min-height:1390px){#D:after{content:" x 1390";}} @media (min-height:1391px){#D:after{content:" x 1391";}} @media (min-height:1392px){#D:after{content:" x 1392";}} @media (min-height:1393px){#D:after{content:" x 1393";}} @media (min-height:1394px){#D:after{content:" x 1394";}} @media (min-height:1395px){#D:after{content:" x 1395";}} @media (min-height:1396px){#D:after{content:" x 1396";}} @media (min-height:1397px){#D:after{content:" x 1397";}} @media (min-height:1398px){#D:after{content:" x 1398";}} @media (min-height:1399px){#D:after{content:" x 1399";}} @media (min-height:1400px){#D:after{content:" x 1400";}} @media (min-height:1401px){#D:after{content:" x 1401";}} @media (min-height:1402px){#D:after{content:" x 1402";}} @media (min-height:1403px){#D:after{content:" x 1403";}} @media (min-height:1404px){#D:after{content:" x 1404";}} @media (min-height:1405px){#D:after{content:" x 1405";}} @media (min-height:1406px){#D:after{content:" x 1406";}} @media (min-height:1407px){#D:after{content:" x 1407";}} @media (min-height:1408px){#D:after{content:" x 1408";}} @media (min-height:1409px){#D:after{content:" x 1409";}} @media (min-height:1410px){#D:after{content:" x 1410";}} @media (min-height:1411px){#D:after{content:" x 1411";}} @media (min-height:1412px){#D:after{content:" x 1412";}} @media (min-height:1413px){#D:after{content:" x 1413";}} @media (min-height:1414px){#D:after{content:" x 1414";}} @media (min-height:1415px){#D:after{content:" x 1415";}} @media (min-height:1416px){#D:after{content:" x 1416";}} @media (min-height:1417px){#D:after{content:" x 1417";}} @media (min-height:1418px){#D:after{content:" x 1418";}} @media (min-height:1419px){#D:after{content:" x 1419";}} @media (min-height:1420px){#D:after{content:" x 1420";}} @media (min-height:1421px){#D:after{content:" x 1421";}} @media (min-height:1422px){#D:after{content:" x 1422";}} @media (min-height:1423px){#D:after{content:" x 1423";}} @media (min-height:1424px){#D:after{content:" x 1424";}} @media (min-height:1425px){#D:after{content:" x 1425";}} @media (min-height:1426px){#D:after{content:" x 1426";}} @media (min-height:1427px){#D:after{content:" x 1427";}} @media (min-height:1428px){#D:after{content:" x 1428";}} @media (min-height:1429px){#D:after{content:" x 1429";}} @media (min-height:1430px){#D:after{content:" x 1430";}} @media (min-height:1431px){#D:after{content:" x 1431";}} @media (min-height:1432px){#D:after{content:" x 1432";}} @media (min-height:1433px){#D:after{content:" x 1433";}} @media (min-height:1434px){#D:after{content:" x 1434";}} @media (min-height:1435px){#D:after{content:" x 1435";}} @media (min-height:1436px){#D:after{content:" x 1436";}} @media (min-height:1437px){#D:after{content:" x 1437";}} @media (min-height:1438px){#D:after{content:" x 1438";}} @media (min-height:1439px){#D:after{content:" x 1439";}} @media (min-height:1440px){#D:after{content:" x 1440";}} @media (min-height:1441px){#D:after{content:" x 1441";}} @media (min-height:1442px){#D:after{content:" x 1442";}} @media (min-height:1443px){#D:after{content:" x 1443";}} @media (min-height:1444px){#D:after{content:" x 1444";}} @media (min-height:1445px){#D:after{content:" x 1445";}} @media (min-height:1446px){#D:after{content:" x 1446";}} @media (min-height:1447px){#D:after{content:" x 1447";}} @media (min-height:1448px){#D:after{content:" x 1448";}} @media (min-height:1449px){#D:after{content:" x 1449";}} @media (min-height:1450px){#D:after{content:" x 1450";}} @media (min-height:1451px){#D:after{content:" x 1451";}} @media (min-height:1452px){#D:after{content:" x 1452";}} @media (min-height:1453px){#D:after{content:" x 1453";}} @media (min-height:1454px){#D:after{content:" x 1454";}} @media (min-height:1455px){#D:after{content:" x 1455";}} @media (min-height:1456px){#D:after{content:" x 1456";}} @media (min-height:1457px){#D:after{content:" x 1457";}} @media (min-height:1458px){#D:after{content:" x 1458";}} @media (min-height:1459px){#D:after{content:" x 1459";}} @media (min-height:1460px){#D:after{content:" x 1460";}} @media (min-height:1461px){#D:after{content:" x 1461";}} @media (min-height:1462px){#D:after{content:" x 1462";}} @media (min-height:1463px){#D:after{content:" x 1463";}} @media (min-height:1464px){#D:after{content:" x 1464";}} @media (min-height:1465px){#D:after{content:" x 1465";}} @media (min-height:1466px){#D:after{content:" x 1466";}} @media (min-height:1467px){#D:after{content:" x 1467";}} @media (min-height:1468px){#D:after{content:" x 1468";}} @media (min-height:1469px){#D:after{content:" x 1469";}} @media (min-height:1470px){#D:after{content:" x 1470";}} @media (min-height:1471px){#D:after{content:" x 1471";}} @media (min-height:1472px){#D:after{content:" x 1472";}} @media (min-height:1473px){#D:after{content:" x 1473";}} @media (min-height:1474px){#D:after{content:" x 1474";}} @media (min-height:1475px){#D:after{content:" x 1475";}} @media (min-height:1476px){#D:after{content:" x 1476";}} @media (min-height:1477px){#D:after{content:" x 1477";}} @media (min-height:1478px){#D:after{content:" x 1478";}} @media (min-height:1479px){#D:after{content:" x 1479";}} @media (min-height:1480px){#D:after{content:" x 1480";}} @media (min-height:1481px){#D:after{content:" x 1481";}} @media (min-height:1482px){#D:after{content:" x 1482";}} @media (min-height:1483px){#D:after{content:" x 1483";}} @media (min-height:1484px){#D:after{content:" x 1484";}} @media (min-height:1485px){#D:after{content:" x 1485";}} @media (min-height:1486px){#D:after{content:" x 1486";}} @media (min-height:1487px){#D:after{content:" x 1487";}} @media (min-height:1488px){#D:after{content:" x 1488";}} @media (min-height:1489px){#D:after{content:" x 1489";}} @media (min-height:1490px){#D:after{content:" x 1490";}} @media (min-height:1491px){#D:after{content:" x 1491";}} @media (min-height:1492px){#D:after{content:" x 1492";}} @media (min-height:1493px){#D:after{content:" x 1493";}} @media (min-height:1494px){#D:after{content:" x 1494";}} @media (min-height:1495px){#D:after{content:" x 1495";}} @media (min-height:1496px){#D:after{content:" x 1496";}} @media (min-height:1497px){#D:after{content:" x 1497";}} @media (min-height:1498px){#D:after{content:" x 1498";}} @media (min-height:1499px){#D:after{content:" x 1499";}} @media (min-height:1500px){#D:after{content:" x 1500";}} @media (min-height:1501px){#D:after{content:" x 1501";}} @media (min-height:1502px){#D:after{content:" x 1502";}} @media (min-height:1503px){#D:after{content:" x 1503";}} @media (min-height:1504px){#D:after{content:" x 1504";}} @media (min-height:1505px){#D:after{content:" x 1505";}} @media (min-height:1506px){#D:after{content:" x 1506";}} @media (min-height:1507px){#D:after{content:" x 1507";}} @media (min-height:1508px){#D:after{content:" x 1508";}} @media (min-height:1509px){#D:after{content:" x 1509";}} @media (min-height:1510px){#D:after{content:" x 1510";}} @media (min-height:1511px){#D:after{content:" x 1511";}} @media (min-height:1512px){#D:after{content:" x 1512";}} @media (min-height:1513px){#D:after{content:" x 1513";}} @media (min-height:1514px){#D:after{content:" x 1514";}} @media (min-height:1515px){#D:after{content:" x 1515";}} @media (min-height:1516px){#D:after{content:" x 1516";}} @media (min-height:1517px){#D:after{content:" x 1517";}} @media (min-height:1518px){#D:after{content:" x 1518";}} @media (min-height:1519px){#D:after{content:" x 1519";}} @media (min-height:1520px){#D:after{content:" x 1520";}} @media (min-height:1521px){#D:after{content:" x 1521";}} @media (min-height:1522px){#D:after{content:" x 1522";}} @media (min-height:1523px){#D:after{content:" x 1523";}} @media (min-height:1524px){#D:after{content:" x 1524";}} @media (min-height:1525px){#D:after{content:" x 1525";}} @media (min-height:1526px){#D:after{content:" x 1526";}} @media (min-height:1527px){#D:after{content:" x 1527";}} @media (min-height:1528px){#D:after{content:" x 1528";}} @media (min-height:1529px){#D:after{content:" x 1529";}} @media (min-height:1530px){#D:after{content:" x 1530";}} @media (min-height:1531px){#D:after{content:" x 1531";}} @media (min-height:1532px){#D:after{content:" x 1532";}} @media (min-height:1533px){#D:after{content:" x 1533";}} @media (min-height:1534px){#D:after{content:" x 1534";}} @media (min-height:1535px){#D:after{content:" x 1535";}} @media (min-height:1536px){#D:after{content:" x 1536";}} @media (min-height:1537px){#D:after{content:" x 1537";}} @media (min-height:1538px){#D:after{content:" x 1538";}} @media (min-height:1539px){#D:after{content:" x 1539";}} @media (min-height:1540px){#D:after{content:" x 1540";}} @media (min-height:1541px){#D:after{content:" x 1541";}} @media (min-height:1542px){#D:after{content:" x 1542";}} @media (min-height:1543px){#D:after{content:" x 1543";}} @media (min-height:1544px){#D:after{content:" x 1544";}} @media (min-height:1545px){#D:after{content:" x 1545";}} @media (min-height:1546px){#D:after{content:" x 1546";}} @media (min-height:1547px){#D:after{content:" x 1547";}} @media (min-height:1548px){#D:after{content:" x 1548";}} @media (min-height:1549px){#D:after{content:" x 1549";}} @media (min-height:1550px){#D:after{content:" x 1550";}} @media (min-height:1551px){#D:after{content:" x 1551";}} @media (min-height:1552px){#D:after{content:" x 1552";}} @media (min-height:1553px){#D:after{content:" x 1553";}} @media (min-height:1554px){#D:after{content:" x 1554";}} @media (min-height:1555px){#D:after{content:" x 1555";}} @media (min-height:1556px){#D:after{content:" x 1556";}} @media (min-height:1557px){#D:after{content:" x 1557";}} @media (min-height:1558px){#D:after{content:" x 1558";}} @media (min-height:1559px){#D:after{content:" x 1559";}} @media (min-height:1560px){#D:after{content:" x 1560";}} @media (min-height:1561px){#D:after{content:" x 1561";}} @media (min-height:1562px){#D:after{content:" x 1562";}} @media (min-height:1563px){#D:after{content:" x 1563";}} @media (min-height:1564px){#D:after{content:" x 1564";}} @media (min-height:1565px){#D:after{content:" x 1565";}} @media (min-height:1566px){#D:after{content:" x 1566";}} @media (min-height:1567px){#D:after{content:" x 1567";}} @media (min-height:1568px){#D:after{content:" x 1568";}} @media (min-height:1569px){#D:after{content:" x 1569";}} @media (min-height:1570px){#D:after{content:" x 1570";}} @media (min-height:1571px){#D:after{content:" x 1571";}} @media (min-height:1572px){#D:after{content:" x 1572";}} @media (min-height:1573px){#D:after{content:" x 1573";}} @media (min-height:1574px){#D:after{content:" x 1574";}} @media (min-height:1575px){#D:after{content:" x 1575";}} @media (min-height:1576px){#D:after{content:" x 1576";}} @media (min-height:1577px){#D:after{content:" x 1577";}} @media (min-height:1578px){#D:after{content:" x 1578";}} @media (min-height:1579px){#D:after{content:" x 1579";}} @media (min-height:1580px){#D:after{content:" x 1580";}} @media (min-height:1581px){#D:after{content:" x 1581";}} @media (min-height:1582px){#D:after{content:" x 1582";}} @media (min-height:1583px){#D:after{content:" x 1583";}} @media (min-height:1584px){#D:after{content:" x 1584";}} @media (min-height:1585px){#D:after{content:" x 1585";}} @media (min-height:1586px){#D:after{content:" x 1586";}} @media (min-height:1587px){#D:after{content:" x 1587";}} @media (min-height:1588px){#D:after{content:" x 1588";}} @media (min-height:1589px){#D:after{content:" x 1589";}} @media (min-height:1590px){#D:after{content:" x 1590";}} @media (min-height:1591px){#D:after{content:" x 1591";}} @media (min-height:1592px){#D:after{content:" x 1592";}} @media (min-height:1593px){#D:after{content:" x 1593";}} @media (min-height:1594px){#D:after{content:" x 1594";}} @media (min-height:1595px){#D:after{content:" x 1595";}} @media (min-height:1596px){#D:after{content:" x 1596";}} @media (min-height:1597px){#D:after{content:" x 1597";}} @media (min-height:1598px){#D:after{content:" x 1598";}} @media (min-height:1599px){#D:after{content:" x 1599";}} @media (min-height:1600px){#D:after{content:" x 1600";}} @media (min-height:1601px){#D:after{content:" x 1601";}} @media (min-height:1602px){#D:after{content:" x 1602";}} @media (min-height:1603px){#D:after{content:" x 1603";}} @media (min-height:1604px){#D:after{content:" x 1604";}} @media (min-height:1605px){#D:after{content:" x 1605";}} @media (min-height:1606px){#D:after{content:" x 1606";}} @media (min-height:1607px){#D:after{content:" x 1607";}} @media (min-height:1608px){#D:after{content:" x 1608";}} @media (min-height:1609px){#D:after{content:" x 1609";}} @media (min-height:1610px){#D:after{content:" x 1610";}} @media (min-height:1611px){#D:after{content:" x 1611";}} @media (min-height:1612px){#D:after{content:" x 1612";}} @media (min-height:1613px){#D:after{content:" x 1613";}} @media (min-height:1614px){#D:after{content:" x 1614";}} @media (min-height:1615px){#D:after{content:" x 1615";}} @media (min-height:1616px){#D:after{content:" x 1616";}} @media (min-height:1617px){#D:after{content:" x 1617";}} @media (min-height:1618px){#D:after{content:" x 1618";}} @media (min-height:1619px){#D:after{content:" x 1619";}} @media (min-height:1620px){#D:after{content:" x 1620";}} @media (min-height:1621px){#D:after{content:" x 1621";}} @media (min-height:1622px){#D:after{content:" x 1622";}} @media (min-height:1623px){#D:after{content:" x 1623";}} @media (min-height:1624px){#D:after{content:" x 1624";}} @media (min-height:1625px){#D:after{content:" x 1625";}} @media (min-height:1626px){#D:after{content:" x 1626";}} @media (min-height:1627px){#D:after{content:" x 1627";}} @media (min-height:1628px){#D:after{content:" x 1628";}} @media (min-height:1629px){#D:after{content:" x 1629";}} @media (min-height:1630px){#D:after{content:" x 1630";}} @media (min-height:1631px){#D:after{content:" x 1631";}} @media (min-height:1632px){#D:after{content:" x 1632";}} @media (min-height:1633px){#D:after{content:" x 1633";}} @media (min-height:1634px){#D:after{content:" x 1634";}} @media (min-height:1635px){#D:after{content:" x 1635";}} @media (min-height:1636px){#D:after{content:" x 1636";}} @media (min-height:1637px){#D:after{content:" x 1637";}} @media (min-height:1638px){#D:after{content:" x 1638";}} @media (min-height:1639px){#D:after{content:" x 1639";}} @media (min-height:1640px){#D:after{content:" x 1640";}} @media (min-height:1641px){#D:after{content:" x 1641";}} @media (min-height:1642px){#D:after{content:" x 1642";}} @media (min-height:1643px){#D:after{content:" x 1643";}} @media (min-height:1644px){#D:after{content:" x 1644";}} @media (min-height:1645px){#D:after{content:" x 1645";}} @media (min-height:1646px){#D:after{content:" x 1646";}} @media (min-height:1647px){#D:after{content:" x 1647";}} @media (min-height:1648px){#D:after{content:" x 1648";}} @media (min-height:1649px){#D:after{content:" x 1649";}} @media (min-height:1650px){#D:after{content:" x 1650";}} @media (min-height:1651px){#D:after{content:" x 1651";}} @media (min-height:1652px){#D:after{content:" x 1652";}} @media (min-height:1653px){#D:after{content:" x 1653";}} @media (min-height:1654px){#D:after{content:" x 1654";}} @media (min-height:1655px){#D:after{content:" x 1655";}} @media (min-height:1656px){#D:after{content:" x 1656";}} @media (min-height:1657px){#D:after{content:" x 1657";}} @media (min-height:1658px){#D:after{content:" x 1658";}} @media (min-height:1659px){#D:after{content:" x 1659";}} @media (min-height:1660px){#D:after{content:" x 1660";}} @media (min-height:1661px){#D:after{content:" x 1661";}} @media (min-height:1662px){#D:after{content:" x 1662";}} @media (min-height:1663px){#D:after{content:" x 1663";}} @media (min-height:1664px){#D:after{content:" x 1664";}} @media (min-height:1665px){#D:after{content:" x 1665";}} @media (min-height:1666px){#D:after{content:" x 1666";}} @media (min-height:1667px){#D:after{content:" x 1667";}} @media (min-height:1668px){#D:after{content:" x 1668";}} @media (min-height:1669px){#D:after{content:" x 1669";}} @media (min-height:1670px){#D:after{content:" x 1670";}} @media (min-height:1671px){#D:after{content:" x 1671";}} @media (min-height:1672px){#D:after{content:" x 1672";}} @media (min-height:1673px){#D:after{content:" x 1673";}} @media (min-height:1674px){#D:after{content:" x 1674";}} @media (min-height:1675px){#D:after{content:" x 1675";}} @media (min-height:1676px){#D:after{content:" x 1676";}} @media (min-height:1677px){#D:after{content:" x 1677";}} @media (min-height:1678px){#D:after{content:" x 1678";}} @media (min-height:1679px){#D:after{content:" x 1679";}} @media (min-height:1680px){#D:after{content:" x 1680";}} @media (min-height:1681px){#D:after{content:" x 1681";}} @media (min-height:1682px){#D:after{content:" x 1682";}} @media (min-height:1683px){#D:after{content:" x 1683";}} @media (min-height:1684px){#D:after{content:" x 1684";}} @media (min-height:1685px){#D:after{content:" x 1685";}} @media (min-height:1686px){#D:after{content:" x 1686";}} @media (min-height:1687px){#D:after{content:" x 1687";}} @media (min-height:1688px){#D:after{content:" x 1688";}} @media (min-height:1689px){#D:after{content:" x 1689";}} @media (min-height:1690px){#D:after{content:" x 1690";}} @media (min-height:1691px){#D:after{content:" x 1691";}} @media (min-height:1692px){#D:after{content:" x 1692";}} @media (min-height:1693px){#D:after{content:" x 1693";}} @media (min-height:1694px){#D:after{content:" x 1694";}} @media (min-height:1695px){#D:after{content:" x 1695";}} @media (min-height:1696px){#D:after{content:" x 1696";}} @media (min-height:1697px){#D:after{content:" x 1697";}} @media (min-height:1698px){#D:after{content:" x 1698";}} @media (min-height:1699px){#D:after{content:" x 1699";}} @media (min-height:1700px){#D:after{content:" x 1700";}} @media (min-height:1701px){#D:after{content:" x 1701";}} @media (min-height:1702px){#D:after{content:" x 1702";}} @media (min-height:1703px){#D:after{content:" x 1703";}} @media (min-height:1704px){#D:after{content:" x 1704";}} @media (min-height:1705px){#D:after{content:" x 1705";}} @media (min-height:1706px){#D:after{content:" x 1706";}} @media (min-height:1707px){#D:after{content:" x 1707";}} @media (min-height:1708px){#D:after{content:" x 1708";}} @media (min-height:1709px){#D:after{content:" x 1709";}} @media (min-height:1710px){#D:after{content:" x 1710";}} @media (min-height:1711px){#D:after{content:" x 1711";}} @media (min-height:1712px){#D:after{content:" x 1712";}} @media (min-height:1713px){#D:after{content:" x 1713";}} @media (min-height:1714px){#D:after{content:" x 1714";}} @media (min-height:1715px){#D:after{content:" x 1715";}} @media (min-height:1716px){#D:after{content:" x 1716";}} @media (min-height:1717px){#D:after{content:" x 1717";}} @media (min-height:1718px){#D:after{content:" x 1718";}} @media (min-height:1719px){#D:after{content:" x 1719";}} @media (min-height:1720px){#D:after{content:" x 1720";}} @media (min-height:1721px){#D:after{content:" x 1721";}} @media (min-height:1722px){#D:after{content:" x 1722";}} @media (min-height:1723px){#D:after{content:" x 1723";}} @media (min-height:1724px){#D:after{content:" x 1724";}} @media (min-height:1725px){#D:after{content:" x 1725";}} @media (min-height:1726px){#D:after{content:" x 1726";}} @media (min-height:1727px){#D:after{content:" x 1727";}} @media (min-height:1728px){#D:after{content:" x 1728";}} @media (min-height:1729px){#D:after{content:" x 1729";}} @media (min-height:1730px){#D:after{content:" x 1730";}} @media (min-height:1731px){#D:after{content:" x 1731";}} @media (min-height:1732px){#D:after{content:" x 1732";}} @media (min-height:1733px){#D:after{content:" x 1733";}} @media (min-height:1734px){#D:after{content:" x 1734";}} @media (min-height:1735px){#D:after{content:" x 1735";}} @media (min-height:1736px){#D:after{content:" x 1736";}} @media (min-height:1737px){#D:after{content:" x 1737";}} @media (min-height:1738px){#D:after{content:" x 1738";}} @media (min-height:1739px){#D:after{content:" x 1739";}} @media (min-height:1740px){#D:after{content:" x 1740";}} @media (min-height:1741px){#D:after{content:" x 1741";}} @media (min-height:1742px){#D:after{content:" x 1742";}} @media (min-height:1743px){#D:after{content:" x 1743";}} @media (min-height:1744px){#D:after{content:" x 1744";}} @media (min-height:1745px){#D:after{content:" x 1745";}} @media (min-height:1746px){#D:after{content:" x 1746";}} @media (min-height:1747px){#D:after{content:" x 1747";}} @media (min-height:1748px){#D:after{content:" x 1748";}} @media (min-height:1749px){#D:after{content:" x 1749";}} @media (min-height:1750px){#D:after{content:" x 1750";}} @media (min-height:1751px){#D:after{content:" x 1751";}} @media (min-height:1752px){#D:after{content:" x 1752";}} @media (min-height:1753px){#D:after{content:" x 1753";}} @media (min-height:1754px){#D:after{content:" x 1754";}} @media (min-height:1755px){#D:after{content:" x 1755";}} @media (min-height:1756px){#D:after{content:" x 1756";}} @media (min-height:1757px){#D:after{content:" x 1757";}} @media (min-height:1758px){#D:after{content:" x 1758";}} @media (min-height:1759px){#D:after{content:" x 1759";}} @media (min-height:1760px){#D:after{content:" x 1760";}} @media (min-height:1761px){#D:after{content:" x 1761";}} @media (min-height:1762px){#D:after{content:" x 1762";}} @media (min-height:1763px){#D:after{content:" x 1763";}} @media (min-height:1764px){#D:after{content:" x 1764";}} @media (min-height:1765px){#D:after{content:" x 1765";}} @media (min-height:1766px){#D:after{content:" x 1766";}} @media (min-height:1767px){#D:after{content:" x 1767";}} @media (min-height:1768px){#D:after{content:" x 1768";}} @media (min-height:1769px){#D:after{content:" x 1769";}} @media (min-height:1770px){#D:after{content:" x 1770";}} @media (min-height:1771px){#D:after{content:" x 1771";}} @media (min-height:1772px){#D:after{content:" x 1772";}} @media (min-height:1773px){#D:after{content:" x 1773";}} @media (min-height:1774px){#D:after{content:" x 1774";}} @media (min-height:1775px){#D:after{content:" x 1775";}} @media (min-height:1776px){#D:after{content:" x 1776";}} @media (min-height:1777px){#D:after{content:" x 1777";}} @media (min-height:1778px){#D:after{content:" x 1778";}} @media (min-height:1779px){#D:after{content:" x 1779";}} @media (min-height:1780px){#D:after{content:" x 1780";}} @media (min-height:1781px){#D:after{content:" x 1781";}} @media (min-height:1782px){#D:after{content:" x 1782";}} @media (min-height:1783px){#D:after{content:" x 1783";}} @media (min-height:1784px){#D:after{content:" x 1784";}} @media (min-height:1785px){#D:after{content:" x 1785";}} @media (min-height:1786px){#D:after{content:" x 1786";}} @media (min-height:1787px){#D:after{content:" x 1787";}} @media (min-height:1788px){#D:after{content:" x 1788";}} @media (min-height:1789px){#D:after{content:" x 1789";}} @media (min-height:1790px){#D:after{content:" x 1790";}} @media (min-height:1791px){#D:after{content:" x 1791";}} @media (min-height:1792px){#D:after{content:" x 1792";}} @media (min-height:1793px){#D:after{content:" x 1793";}} @media (min-height:1794px){#D:after{content:" x 1794";}} @media (min-height:1795px){#D:after{content:" x 1795";}} @media (min-height:1796px){#D:after{content:" x 1796";}} @media (min-height:1797px){#D:after{content:" x 1797";}} @media (min-height:1798px){#D:after{content:" x 1798";}} @media (min-height:1799px){#D:after{content:" x 1799";}} @media (min-height:1800px){#D:after{content:" x 1800";}} @media (min-height:1801px){#D:after{content:" x 1801";}} @media (min-height:1802px){#D:after{content:" x 1802";}} @media (min-height:1803px){#D:after{content:" x 1803";}} @media (min-height:1804px){#D:after{content:" x 1804";}} @media (min-height:1805px){#D:after{content:" x 1805";}} @media (min-height:1806px){#D:after{content:" x 1806";}} @media (min-height:1807px){#D:after{content:" x 1807";}} @media (min-height:1808px){#D:after{content:" x 1808";}} @media (min-height:1809px){#D:after{content:" x 1809";}} @media (min-height:1810px){#D:after{content:" x 1810";}} @media (min-height:1811px){#D:after{content:" x 1811";}} @media (min-height:1812px){#D:after{content:" x 1812";}} @media (min-height:1813px){#D:after{content:" x 1813";}} @media (min-height:1814px){#D:after{content:" x 1814";}} @media (min-height:1815px){#D:after{content:" x 1815";}} @media (min-height:1816px){#D:after{content:" x 1816";}} @media (min-height:1817px){#D:after{content:" x 1817";}} @media (min-height:1818px){#D:after{content:" x 1818";}} @media (min-height:1819px){#D:after{content:" x 1819";}} @media (min-height:1820px){#D:after{content:" x 1820";}} @media (min-height:1821px){#D:after{content:" x 1821";}} @media (min-height:1822px){#D:after{content:" x 1822";}} @media (min-height:1823px){#D:after{content:" x 1823";}} @media (min-height:1824px){#D:after{content:" x 1824";}} @media (min-height:1825px){#D:after{content:" x 1825";}} @media (min-height:1826px){#D:after{content:" x 1826";}} @media (min-height:1827px){#D:after{content:" x 1827";}} @media (min-height:1828px){#D:after{content:" x 1828";}} @media (min-height:1829px){#D:after{content:" x 1829";}} @media (min-height:1830px){#D:after{content:" x 1830";}} @media (min-height:1831px){#D:after{content:" x 1831";}} @media (min-height:1832px){#D:after{content:" x 1832";}} @media (min-height:1833px){#D:after{content:" x 1833";}} @media (min-height:1834px){#D:after{content:" x 1834";}} @media (min-height:1835px){#D:after{content:" x 1835";}} @media (min-height:1836px){#D:after{content:" x 1836";}} @media (min-height:1837px){#D:after{content:" x 1837";}} @media (min-height:1838px){#D:after{content:" x 1838";}} @media (min-height:1839px){#D:after{content:" x 1839";}} @media (min-height:1840px){#D:after{content:" x 1840";}} @media (min-height:1841px){#D:after{content:" x 1841";}} @media (min-height:1842px){#D:after{content:" x 1842";}} @media (min-height:1843px){#D:after{content:" x 1843";}} @media (min-height:1844px){#D:after{content:" x 1844";}} @media (min-height:1845px){#D:after{content:" x 1845";}} @media (min-height:1846px){#D:after{content:" x 1846";}} @media (min-height:1847px){#D:after{content:" x 1847";}} @media (min-height:1848px){#D:after{content:" x 1848";}} @media (min-height:1849px){#D:after{content:" x 1849";}} @media (min-height:1850px){#D:after{content:" x 1850";}} @media (min-height:1851px){#D:after{content:" x 1851";}} @media (min-height:1852px){#D:after{content:" x 1852";}} @media (min-height:1853px){#D:after{content:" x 1853";}} @media (min-height:1854px){#D:after{content:" x 1854";}} @media (min-height:1855px){#D:after{content:" x 1855";}} @media (min-height:1856px){#D:after{content:" x 1856";}} @media (min-height:1857px){#D:after{content:" x 1857";}} @media (min-height:1858px){#D:after{content:" x 1858";}} @media (min-height:1859px){#D:after{content:" x 1859";}} @media (min-height:1860px){#D:after{content:" x 1860";}} @media (min-height:1861px){#D:after{content:" x 1861";}} @media (min-height:1862px){#D:after{content:" x 1862";}} @media (min-height:1863px){#D:after{content:" x 1863";}} @media (min-height:1864px){#D:after{content:" x 1864";}} @media (min-height:1865px){#D:after{content:" x 1865";}} @media (min-height:1866px){#D:after{content:" x 1866";}} @media (min-height:1867px){#D:after{content:" x 1867";}} @media (min-height:1868px){#D:after{content:" x 1868";}} @media (min-height:1869px){#D:after{content:" x 1869";}} @media (min-height:1870px){#D:after{content:" x 1870";}} @media (min-height:1871px){#D:after{content:" x 1871";}} @media (min-height:1872px){#D:after{content:" x 1872";}} @media (min-height:1873px){#D:after{content:" x 1873";}} @media (min-height:1874px){#D:after{content:" x 1874";}} @media (min-height:1875px){#D:after{content:" x 1875";}} @media (min-height:1876px){#D:after{content:" x 1876";}} @media (min-height:1877px){#D:after{content:" x 1877";}} @media (min-height:1878px){#D:after{content:" x 1878";}} @media (min-height:1879px){#D:after{content:" x 1879";}} @media (min-height:1880px){#D:after{content:" x 1880";}} @media (min-height:1881px){#D:after{content:" x 1881";}} @media (min-height:1882px){#D:after{content:" x 1882";}} @media (min-height:1883px){#D:after{content:" x 1883";}} @media (min-height:1884px){#D:after{content:" x 1884";}} @media (min-height:1885px){#D:after{content:" x 1885";}} @media (min-height:1886px){#D:after{content:" x 1886";}} @media (min-height:1887px){#D:after{content:" x 1887";}} @media (min-height:1888px){#D:after{content:" x 1888";}} @media (min-height:1889px){#D:after{content:" x 1889";}} @media (min-height:1890px){#D:after{content:" x 1890";}} @media (min-height:1891px){#D:after{content:" x 1891";}} @media (min-height:1892px){#D:after{content:" x 1892";}} @media (min-height:1893px){#D:after{content:" x 1893";}} @media (min-height:1894px){#D:after{content:" x 1894";}} @media (min-height:1895px){#D:after{content:" x 1895";}} @media (min-height:1896px){#D:after{content:" x 1896";}} @media (min-height:1897px){#D:after{content:" x 1897";}} @media (min-height:1898px){#D:after{content:" x 1898";}} @media (min-height:1899px){#D:after{content:" x 1899";}} @media (min-height:1900px){#D:after{content:" x 1900";}} @media (min-height:1901px){#D:after{content:" x 1901";}} @media (min-height:1902px){#D:after{content:" x 1902";}} @media (min-height:1903px){#D:after{content:" x 1903";}} @media (min-height:1904px){#D:after{content:" x 1904";}} @media (min-height:1905px){#D:after{content:" x 1905";}} @media (min-height:1906px){#D:after{content:" x 1906";}} @media (min-height:1907px){#D:after{content:" x 1907";}} @media (min-height:1908px){#D:after{content:" x 1908";}} @media (min-height:1909px){#D:after{content:" x 1909";}} @media (min-height:1910px){#D:after{content:" x 1910";}} @media (min-height:1911px){#D:after{content:" x 1911";}} @media (min-height:1912px){#D:after{content:" x 1912";}} @media (min-height:1913px){#D:after{content:" x 1913";}} @media (min-height:1914px){#D:after{content:" x 1914";}} @media (min-height:1915px){#D:after{content:" x 1915";}} @media (min-height:1916px){#D:after{content:" x 1916";}} @media (min-height:1917px){#D:after{content:" x 1917";}} @media (min-height:1918px){#D:after{content:" x 1918";}} @media (min-height:1919px){#D:after{content:" x 1919";}} @media (min-height:1920px){#D:after{content:" x 1920";}} @media (min-height:1921px){#D:after{content:" x 1921";}} @media (min-height:1922px){#D:after{content:" x 1922";}} @media (min-height:1923px){#D:after{content:" x 1923";}} @media (min-height:1924px){#D:after{content:" x 1924";}} @media (min-height:1925px){#D:after{content:" x 1925";}} @media (min-height:1926px){#D:after{content:" x 1926";}} @media (min-height:1927px){#D:after{content:" x 1927";}} @media (min-height:1928px){#D:after{content:" x 1928";}} @media (min-height:1929px){#D:after{content:" x 1929";}} @media (min-height:1930px){#D:after{content:" x 1930";}} @media (min-height:1931px){#D:after{content:" x 1931";}} @media (min-height:1932px){#D:after{content:" x 1932";}} @media (min-height:1933px){#D:after{content:" x 1933";}} @media (min-height:1934px){#D:after{content:" x 1934";}} @media (min-height:1935px){#D:after{content:" x 1935";}} @media (min-height:1936px){#D:after{content:" x 1936";}} @media (min-height:1937px){#D:after{content:" x 1937";}} @media (min-height:1938px){#D:after{content:" x 1938";}} @media (min-height:1939px){#D:after{content:" x 1939";}} @media (min-height:1940px){#D:after{content:" x 1940";}} @media (min-height:1941px){#D:after{content:" x 1941";}} @media (min-height:1942px){#D:after{content:" x 1942";}} @media (min-height:1943px){#D:after{content:" x 1943";}} @media (min-height:1944px){#D:after{content:" x 1944";}} @media (min-height:1945px){#D:after{content:" x 1945";}} @media (min-height:1946px){#D:after{content:" x 1946";}} @media (min-height:1947px){#D:after{content:" x 1947";}} @media (min-height:1948px){#D:after{content:" x 1948";}} @media (min-height:1949px){#D:after{content:" x 1949";}} @media (min-height:1950px){#D:after{content:" x 1950";}} @media (min-height:1951px){#D:after{content:" x 1951";}} @media (min-height:1952px){#D:after{content:" x 1952";}} @media (min-height:1953px){#D:after{content:" x 1953";}} @media (min-height:1954px){#D:after{content:" x 1954";}} @media (min-height:1955px){#D:after{content:" x 1955";}} @media (min-height:1956px){#D:after{content:" x 1956";}} @media (min-height:1957px){#D:after{content:" x 1957";}} @media (min-height:1958px){#D:after{content:" x 1958";}} @media (min-height:1959px){#D:after{content:" x 1959";}} @media (min-height:1960px){#D:after{content:" x 1960";}} @media (min-height:1961px){#D:after{content:" x 1961";}} @media (min-height:1962px){#D:after{content:" x 1962";}} @media (min-height:1963px){#D:after{content:" x 1963";}} @media (min-height:1964px){#D:after{content:" x 1964";}} @media (min-height:1965px){#D:after{content:" x 1965";}} @media (min-height:1966px){#D:after{content:" x 1966";}} @media (min-height:1967px){#D:after{content:" x 1967";}} @media (min-height:1968px){#D:after{content:" x 1968";}} @media (min-height:1969px){#D:after{content:" x 1969";}} @media (min-height:1970px){#D:after{content:" x 1970";}} @media (min-height:1971px){#D:after{content:" x 1971";}} @media (min-height:1972px){#D:after{content:" x 1972";}} @media (min-height:1973px){#D:after{content:" x 1973";}} @media (min-height:1974px){#D:after{content:" x 1974";}} @media (min-height:1975px){#D:after{content:" x 1975";}} @media (min-height:1976px){#D:after{content:" x 1976";}} @media (min-height:1977px){#D:after{content:" x 1977";}} @media (min-height:1978px){#D:after{content:" x 1978";}} @media (min-height:1979px){#D:after{content:" x 1979";}} @media (min-height:1980px){#D:after{content:" x 1980";}} @media (min-height:1981px){#D:after{content:" x 1981";}} @media (min-height:1982px){#D:after{content:" x 1982";}} @media (min-height:1983px){#D:after{content:" x 1983";}} @media (min-height:1984px){#D:after{content:" x 1984";}} @media (min-height:1985px){#D:after{content:" x 1985";}} @media (min-height:1986px){#D:after{content:" x 1986";}} @media (min-height:1987px){#D:after{content:" x 1987";}} @media (min-height:1988px){#D:after{content:" x 1988";}} @media (min-height:1989px){#D:after{content:" x 1989";}} @media (min-height:1990px){#D:after{content:" x 1990";}} @media (min-height:1991px){#D:after{content:" x 1991";}} @media (min-height:1992px){#D:after{content:" x 1992";}} @media (min-height:1993px){#D:after{content:" x 1993";}} @media (min-height:1994px){#D:after{content:" x 1994";}} @media (min-height:1995px){#D:after{content:" x 1995";}} @media (min-height:1996px){#D:after{content:" x 1996";}} @media (min-height:1997px){#D:after{content:" x 1997";}} @media (min-height:1998px){#D:after{content:" x 1998";}} @media (min-height:1999px){#D:after{content:" x 1999";}} @media (min-height:2000px){#D:after{content:" x 2000";}} @media (min-height:2001px){#D:after{content:" x 2001";}} @media (min-height:2002px){#D:after{content:" x 2002";}} @media (min-height:2003px){#D:after{content:" x 2003";}} @media (min-height:2004px){#D:after{content:" x 2004";}} @media (min-height:2005px){#D:after{content:" x 2005";}} @media (min-height:2006px){#D:after{content:" x 2006";}} @media (min-height:2007px){#D:after{content:" x 2007";}} @media (min-height:2008px){#D:after{content:" x 2008";}} @media (min-height:2009px){#D:after{content:" x 2009";}} @media (min-height:2010px){#D:after{content:" x 2010";}} @media (min-height:2011px){#D:after{content:" x 2011";}} @media (min-height:2012px){#D:after{content:" x 2012";}} @media (min-height:2013px){#D:after{content:" x 2013";}} @media (min-height:2014px){#D:after{content:" x 2014";}} @media (min-height:2015px){#D:after{content:" x 2015";}} @media (min-height:2016px){#D:after{content:" x 2016";}} @media (min-height:2017px){#D:after{content:" x 2017";}} @media (min-height:2018px){#D:after{content:" x 2018";}} @media (min-height:2019px){#D:after{content:" x 2019";}} @media (min-height:2020px){#D:after{content:" x 2020";}} @media (min-height:2021px){#D:after{content:" x 2021";}} @media (min-height:2022px){#D:after{content:" x 2022";}} @media (min-height:2023px){#D:after{content:" x 2023";}} @media (min-height:2024px){#D:after{content:" x 2024";}} @media (min-height:2025px){#D:after{content:" x 2025";}} @media (min-height:2026px){#D:after{content:" x 2026";}} @media (min-height:2027px){#D:after{content:" x 2027";}} @media (min-height:2028px){#D:after{content:" x 2028";}} @media (min-height:2029px){#D:after{content:" x 2029";}} @media (min-height:2030px){#D:after{content:" x 2030";}} @media (min-height:2031px){#D:after{content:" x 2031";}} @media (min-height:2032px){#D:after{content:" x 2032";}} @media (min-height:2033px){#D:after{content:" x 2033";}} @media (min-height:2034px){#D:after{content:" x 2034";}} @media (min-height:2035px){#D:after{content:" x 2035";}} @media (min-height:2036px){#D:after{content:" x 2036";}} @media (min-height:2037px){#D:after{content:" x 2037";}} @media (min-height:2038px){#D:after{content:" x 2038";}} @media (min-height:2039px){#D:after{content:" x 2039";}} @media (min-height:2040px){#D:after{content:" x 2040";}} @media (min-height:2041px){#D:after{content:" x 2041";}} @media (min-height:2042px){#D:after{content:" x 2042";}} @media (min-height:2043px){#D:after{content:" x 2043";}} @media (min-height:2044px){#D:after{content:" x 2044";}} @media (min-height:2045px){#D:after{content:" x 2045";}} @media (min-height:2046px){#D:after{content:" x 2046";}} @media (min-height:2047px){#D:after{content:" x 2047";}} @media (min-height:2048px){#D:after{content:" x 2048";}} @media (min-height:2049px){#D:after{content:" x 2049";}} @media (min-height:2050px){#D:after{content:" x 2050";}} @media (min-height:2051px){#D:after{content:" x 2051";}} @media (min-height:2052px){#D:after{content:" x 2052";}} @media (min-height:2053px){#D:after{content:" x 2053";}} @media (min-height:2054px){#D:after{content:" x 2054";}} @media (min-height:2055px){#D:after{content:" x 2055";}} @media (min-height:2056px){#D:after{content:" x 2056";}} @media (min-height:2057px){#D:after{content:" x 2057";}} @media (min-height:2058px){#D:after{content:" x 2058";}} @media (min-height:2059px){#D:after{content:" x 2059";}} @media (min-height:2060px){#D:after{content:" x 2060";}} @media (min-height:2061px){#D:after{content:" x 2061";}} @media (min-height:2062px){#D:after{content:" x 2062";}} @media (min-height:2063px){#D:after{content:" x 2063";}} @media (min-height:2064px){#D:after{content:" x 2064";}} @media (min-height:2065px){#D:after{content:" x 2065";}} @media (min-height:2066px){#D:after{content:" x 2066";}} @media (min-height:2067px){#D:after{content:" x 2067";}} @media (min-height:2068px){#D:after{content:" x 2068";}} @media (min-height:2069px){#D:after{content:" x 2069";}} @media (min-height:2070px){#D:after{content:" x 2070";}} @media (min-height:2071px){#D:after{content:" x 2071";}} @media (min-height:2072px){#D:after{content:" x 2072";}} @media (min-height:2073px){#D:after{content:" x 2073";}} @media (min-height:2074px){#D:after{content:" x 2074";}} @media (min-height:2075px){#D:after{content:" x 2075";}} @media (min-height:2076px){#D:after{content:" x 2076";}} @media (min-height:2077px){#D:after{content:" x 2077";}} @media (min-height:2078px){#D:after{content:" x 2078";}} @media (min-height:2079px){#D:after{content:" x 2079";}} @media (min-height:2080px){#D:after{content:" x 2080";}} @media (min-height:2081px){#D:after{content:" x 2081";}} @media (min-height:2082px){#D:after{content:" x 2082";}} @media (min-height:2083px){#D:after{content:" x 2083";}} @media (min-height:2084px){#D:after{content:" x 2084";}} @media (min-height:2085px){#D:after{content:" x 2085";}} @media (min-height:2086px){#D:after{content:" x 2086";}} @media (min-height:2087px){#D:after{content:" x 2087";}} @media (min-height:2088px){#D:after{content:" x 2088";}} @media (min-height:2089px){#D:after{content:" x 2089";}} @media (min-height:2090px){#D:after{content:" x 2090";}} @media (min-height:2091px){#D:after{content:" x 2091";}} @media (min-height:2092px){#D:after{content:" x 2092";}} @media (min-height:2093px){#D:after{content:" x 2093";}} @media (min-height:2094px){#D:after{content:" x 2094";}} @media (min-height:2095px){#D:after{content:" x 2095";}} @media (min-height:2096px){#D:after{content:" x 2096";}} @media (min-height:2097px){#D:after{content:" x 2097";}} @media (min-height:2098px){#D:after{content:" x 2098";}} @media (min-height:2099px){#D:after{content:" x 2099";}} @media (min-height:2100px){#D:after{content:" x 2100";}} @media (min-height:2101px){#D:after{content:" x 2101";}} @media (min-height:2102px){#D:after{content:" x 2102";}} @media (min-height:2103px){#D:after{content:" x 2103";}} @media (min-height:2104px){#D:after{content:" x 2104";}} @media (min-height:2105px){#D:after{content:" x 2105";}} @media (min-height:2106px){#D:after{content:" x 2106";}} @media (min-height:2107px){#D:after{content:" x 2107";}} @media (min-height:2108px){#D:after{content:" x 2108";}} @media (min-height:2109px){#D:after{content:" x 2109";}} @media (min-height:2110px){#D:after{content:" x 2110";}} @media (min-height:2111px){#D:after{content:" x 2111";}} @media (min-height:2112px){#D:after{content:" x 2112";}} @media (min-height:2113px){#D:after{content:" x 2113";}} @media (min-height:2114px){#D:after{content:" x 2114";}} @media (min-height:2115px){#D:after{content:" x 2115";}} @media (min-height:2116px){#D:after{content:" x 2116";}} @media (min-height:2117px){#D:after{content:" x 2117";}} @media (min-height:2118px){#D:after{content:" x 2118";}} @media (min-height:2119px){#D:after{content:" x 2119";}} @media (min-height:2120px){#D:after{content:" x 2120";}} @media (min-height:2121px){#D:after{content:" x 2121";}} @media (min-height:2122px){#D:after{content:" x 2122";}} @media (min-height:2123px){#D:after{content:" x 2123";}} @media (min-height:2124px){#D:after{content:" x 2124";}} @media (min-height:2125px){#D:after{content:" x 2125";}} @media (min-height:2126px){#D:after{content:" x 2126";}} @media (min-height:2127px){#D:after{content:" x 2127";}} @media (min-height:2128px){#D:after{content:" x 2128";}} @media (min-height:2129px){#D:after{content:" x 2129";}} @media (min-height:2130px){#D:after{content:" x 2130";}} @media (min-height:2131px){#D:after{content:" x 2131";}} @media (min-height:2132px){#D:after{content:" x 2132";}} @media (min-height:2133px){#D:after{content:" x 2133";}} @media (min-height:2134px){#D:after{content:" x 2134";}} @media (min-height:2135px){#D:after{content:" x 2135";}} @media (min-height:2136px){#D:after{content:" x 2136";}} @media (min-height:2137px){#D:after{content:" x 2137";}} @media (min-height:2138px){#D:after{content:" x 2138";}} @media (min-height:2139px){#D:after{content:" x 2139";}} @media (min-height:2140px){#D:after{content:" x 2140";}} @media (min-height:2141px){#D:after{content:" x 2141";}} @media (min-height:2142px){#D:after{content:" x 2142";}} @media (min-height:2143px){#D:after{content:" x 2143";}} @media (min-height:2144px){#D:after{content:" x 2144";}} @media (min-height:2145px){#D:after{content:" x 2145";}} @media (min-height:2146px){#D:after{content:" x 2146";}} @media (min-height:2147px){#D:after{content:" x 2147";}} @media (min-height:2148px){#D:after{content:" x 2148";}} @media (min-height:2149px){#D:after{content:" x 2149";}} @media (min-height:2150px){#D:after{content:" x 2150";}} @media (min-height:2151px){#D:after{content:" x 2151";}} @media (min-height:2152px){#D:after{content:" x 2152";}} @media (min-height:2153px){#D:after{content:" x 2153";}} @media (min-height:2154px){#D:after{content:" x 2154";}} @media (min-height:2155px){#D:after{content:" x 2155";}} @media (min-height:2156px){#D:after{content:" x 2156";}} @media (min-height:2157px){#D:after{content:" x 2157";}} @media (min-height:2158px){#D:after{content:" x 2158";}} @media (min-height:2159px){#D:after{content:" x 2159";}} @media (min-height:2160px){#D:after{content:" x 2160";}} @media (min-height:2161px){#D:after{content:" x 2161";}} @media (min-height:2162px){#D:after{content:" x 2162";}} @media (min-height:2163px){#D:after{content:" x 2163";}} @media (min-height:2164px){#D:after{content:" x 2164";}} @media (min-height:2165px){#D:after{content:" x 2165";}} @media (min-height:2166px){#D:after{content:" x 2166";}} @media (min-height:2167px){#D:after{content:" x 2167";}} @media (min-height:2168px){#D:after{content:" x 2168";}} @media (min-height:2169px){#D:after{content:" x 2169";}} @media (min-height:2170px){#D:after{content:" x 2170";}} @media (min-height:2171px){#D:after{content:" x 2171";}} @media (min-height:2172px){#D:after{content:" x 2172";}} @media (min-height:2173px){#D:after{content:" x 2173";}} @media (min-height:2174px){#D:after{content:" x 2174";}} @media (min-height:2175px){#D:after{content:" x 2175";}} @media (min-height:2176px){#D:after{content:" x 2176";}} @media (min-height:2177px){#D:after{content:" x 2177";}} @media (min-height:2178px){#D:after{content:" x 2178";}} @media (min-height:2179px){#D:after{content:" x 2179";}} @media (min-height:2180px){#D:after{content:" x 2180";}} @media (min-height:2181px){#D:after{content:" x 2181";}} @media (min-height:2182px){#D:after{content:" x 2182";}} @media (min-height:2183px){#D:after{content:" x 2183";}} @media (min-height:2184px){#D:after{content:" x 2184";}} @media (min-height:2185px){#D:after{content:" x 2185";}} @media (min-height:2186px){#D:after{content:" x 2186";}} @media (min-height:2187px){#D:after{content:" x 2187";}} @media (min-height:2188px){#D:after{content:" x 2188";}} @media (min-height:2189px){#D:after{content:" x 2189";}} @media (min-height:2190px){#D:after{content:" x 2190";}} @media (min-height:2191px){#D:after{content:" x 2191";}} @media (min-height:2192px){#D:after{content:" x 2192";}} @media (min-height:2193px){#D:after{content:" x 2193";}} @media (min-height:2194px){#D:after{content:" x 2194";}} @media (min-height:2195px){#D:after{content:" x 2195";}} @media (min-height:2196px){#D:after{content:" x 2196";}} @media (min-height:2197px){#D:after{content:" x 2197";}} @media (min-height:2198px){#D:after{content:" x 2198";}} @media (min-height:2199px){#D:after{content:" x 2199";}} @media (min-height:2200px){#D:after{content:" x 2200";}} @media (min-height:2201px){#D:after{content:" x 2201";}} @media (min-height:2202px){#D:after{content:" x 2202";}} @media (min-height:2203px){#D:after{content:" x 2203";}} @media (min-height:2204px){#D:after{content:" x 2204";}} @media (min-height:2205px){#D:after{content:" x 2205";}} @media (min-height:2206px){#D:after{content:" x 2206";}} @media (min-height:2207px){#D:after{content:" x 2207";}} @media (min-height:2208px){#D:after{content:" x 2208";}} @media (min-height:2209px){#D:after{content:" x 2209";}} @media (min-height:2210px){#D:after{content:" x 2210";}} @media (min-height:2211px){#D:after{content:" x 2211";}} @media (min-height:2212px){#D:after{content:" x 2212";}} @media (min-height:2213px){#D:after{content:" x 2213";}} @media (min-height:2214px){#D:after{content:" x 2214";}} @media (min-height:2215px){#D:after{content:" x 2215";}} @media (min-height:2216px){#D:after{content:" x 2216";}} @media (min-height:2217px){#D:after{content:" x 2217";}} @media (min-height:2218px){#D:after{content:" x 2218";}} @media (min-height:2219px){#D:after{content:" x 2219";}} @media (min-height:2220px){#D:after{content:" x 2220";}} @media (min-height:2221px){#D:after{content:" x 2221";}} @media (min-height:2222px){#D:after{content:" x 2222";}} @media (min-height:2223px){#D:after{content:" x 2223";}} @media (min-height:2224px){#D:after{content:" x 2224";}} @media (min-height:2225px){#D:after{content:" x 2225";}} @media (min-height:2226px){#D:after{content:" x 2226";}} @media (min-height:2227px){#D:after{content:" x 2227";}} @media (min-height:2228px){#D:after{content:" x 2228";}} @media (min-height:2229px){#D:after{content:" x 2229";}} @media (min-height:2230px){#D:after{content:" x 2230";}} @media (min-height:2231px){#D:after{content:" x 2231";}} @media (min-height:2232px){#D:after{content:" x 2232";}} @media (min-height:2233px){#D:after{content:" x 2233";}} @media (min-height:2234px){#D:after{content:" x 2234";}} @media (min-height:2235px){#D:after{content:" x 2235";}} @media (min-height:2236px){#D:after{content:" x 2236";}} @media (min-height:2237px){#D:after{content:" x 2237";}} @media (min-height:2238px){#D:after{content:" x 2238";}} @media (min-height:2239px){#D:after{content:" x 2239";}} @media (min-height:2240px){#D:after{content:" x 2240";}} @media (min-height:2241px){#D:after{content:" x 2241";}} @media (min-height:2242px){#D:after{content:" x 2242";}} @media (min-height:2243px){#D:after{content:" x 2243";}} @media (min-height:2244px){#D:after{content:" x 2244";}} @media (min-height:2245px){#D:after{content:" x 2245";}} @media (min-height:2246px){#D:after{content:" x 2246";}} @media (min-height:2247px){#D:after{content:" x 2247";}} @media (min-height:2248px){#D:after{content:" x 2248";}} @media (min-height:2249px){#D:after{content:" x 2249";}} @media (min-height:2250px){#D:after{content:" x 2250";}} @media (min-height:2251px){#D:after{content:" x 2251";}} @media (min-height:2252px){#D:after{content:" x 2252";}} @media (min-height:2253px){#D:after{content:" x 2253";}} @media (min-height:2254px){#D:after{content:" x 2254";}} @media (min-height:2255px){#D:after{content:" x 2255";}} @media (min-height:2256px){#D:after{content:" x 2256";}} @media (min-height:2257px){#D:after{content:" x 2257";}} @media (min-height:2258px){#D:after{content:" x 2258";}} @media (min-height:2259px){#D:after{content:" x 2259";}} @media (min-height:2260px){#D:after{content:" x 2260";}} @media (min-height:2261px){#D:after{content:" x 2261";}} @media (min-height:2262px){#D:after{content:" x 2262";}} @media (min-height:2263px){#D:after{content:" x 2263";}} @media (min-height:2264px){#D:after{content:" x 2264";}} @media (min-height:2265px){#D:after{content:" x 2265";}} @media (min-height:2266px){#D:after{content:" x 2266";}} @media (min-height:2267px){#D:after{content:" x 2267";}} @media (min-height:2268px){#D:after{content:" x 2268";}} @media (min-height:2269px){#D:after{content:" x 2269";}} @media (min-height:2270px){#D:after{content:" x 2270";}} @media (min-height:2271px){#D:after{content:" x 2271";}} @media (min-height:2272px){#D:after{content:" x 2272";}} @media (min-height:2273px){#D:after{content:" x 2273";}} @media (min-height:2274px){#D:after{content:" x 2274";}} @media (min-height:2275px){#D:after{content:" x 2275";}} @media (min-height:2276px){#D:after{content:" x 2276";}} @media (min-height:2277px){#D:after{content:" x 2277";}} @media (min-height:2278px){#D:after{content:" x 2278";}} @media (min-height:2279px){#D:after{content:" x 2279";}} @media (min-height:2280px){#D:after{content:" x 2280";}} @media (min-height:2281px){#D:after{content:" x 2281";}} @media (min-height:2282px){#D:after{content:" x 2282";}} @media (min-height:2283px){#D:after{content:" x 2283";}} @media (min-height:2284px){#D:after{content:" x 2284";}} @media (min-height:2285px){#D:after{content:" x 2285";}} @media (min-height:2286px){#D:after{content:" x 2286";}} @media (min-height:2287px){#D:after{content:" x 2287";}} @media (min-height:2288px){#D:after{content:" x 2288";}} @media (min-height:2289px){#D:after{content:" x 2289";}} @media (min-height:2290px){#D:after{content:" x 2290";}} @media (min-height:2291px){#D:after{content:" x 2291";}} @media (min-height:2292px){#D:after{content:" x 2292";}} @media (min-height:2293px){#D:after{content:" x 2293";}} @media (min-height:2294px){#D:after{content:" x 2294";}} @media (min-height:2295px){#D:after{content:" x 2295";}} @media (min-height:2296px){#D:after{content:" x 2296";}} @media (min-height:2297px){#D:after{content:" x 2297";}} @media (min-height:2298px){#D:after{content:" x 2298";}} @media (min-height:2299px){#D:after{content:" x 2299";}} @media (min-height:2300px){#D:after{content:" x 2300";}} @media (min-height:2301px){#D:after{content:" x 2301";}} @media (min-height:2302px){#D:after{content:" x 2302";}} @media (min-height:2303px){#D:after{content:" x 2303";}} @media (min-height:2304px){#D:after{content:" x 2304";}} @media (min-height:2305px){#D:after{content:" x 2305";}} @media (min-height:2306px){#D:after{content:" x 2306";}} @media (min-height:2307px){#D:after{content:" x 2307";}} @media (min-height:2308px){#D:after{content:" x 2308";}} @media (min-height:2309px){#D:after{content:" x 2309";}} @media (min-height:2310px){#D:after{content:" x 2310";}} @media (min-height:2311px){#D:after{content:" x 2311";}} @media (min-height:2312px){#D:after{content:" x 2312";}} @media (min-height:2313px){#D:after{content:" x 2313";}} @media (min-height:2314px){#D:after{content:" x 2314";}} @media (min-height:2315px){#D:after{content:" x 2315";}} @media (min-height:2316px){#D:after{content:" x 2316";}} @media (min-height:2317px){#D:after{content:" x 2317";}} @media (min-height:2318px){#D:after{content:" x 2318";}} @media (min-height:2319px){#D:after{content:" x 2319";}} @media (min-height:2320px){#D:after{content:" x 2320";}} @media (min-height:2321px){#D:after{content:" x 2321";}} @media (min-height:2322px){#D:after{content:" x 2322";}} @media (min-height:2323px){#D:after{content:" x 2323";}} @media (min-height:2324px){#D:after{content:" x 2324";}} @media (min-height:2325px){#D:after{content:" x 2325";}} @media (min-height:2326px){#D:after{content:" x 2326";}} @media (min-height:2327px){#D:after{content:" x 2327";}} @media (min-height:2328px){#D:after{content:" x 2328";}} @media (min-height:2329px){#D:after{content:" x 2329";}} @media (min-height:2330px){#D:after{content:" x 2330";}} @media (min-height:2331px){#D:after{content:" x 2331";}} @media (min-height:2332px){#D:after{content:" x 2332";}} @media (min-height:2333px){#D:after{content:" x 2333";}} @media (min-height:2334px){#D:after{content:" x 2334";}} @media (min-height:2335px){#D:after{content:" x 2335";}} @media (min-height:2336px){#D:after{content:" x 2336";}} @media (min-height:2337px){#D:after{content:" x 2337";}} @media (min-height:2338px){#D:after{content:" x 2338";}} @media (min-height:2339px){#D:after{content:" x 2339";}} @media (min-height:2340px){#D:after{content:" x 2340";}} @media (min-height:2341px){#D:after{content:" x 2341";}} @media (min-height:2342px){#D:after{content:" x 2342";}} @media (min-height:2343px){#D:after{content:" x 2343";}} @media (min-height:2344px){#D:after{content:" x 2344";}} @media (min-height:2345px){#D:after{content:" x 2345";}} @media (min-height:2346px){#D:after{content:" x 2346";}} @media (min-height:2347px){#D:after{content:" x 2347";}} @media (min-height:2348px){#D:after{content:" x 2348";}} @media (min-height:2349px){#D:after{content:" x 2349";}} @media (min-height:2350px){#D:after{content:" x 2350";}} @media (min-height:2351px){#D:after{content:" x 2351";}} @media (min-height:2352px){#D:after{content:" x 2352";}} @media (min-height:2353px){#D:after{content:" x 2353";}} @media (min-height:2354px){#D:after{content:" x 2354";}} @media (min-height:2355px){#D:after{content:" x 2355";}} @media (min-height:2356px){#D:after{content:" x 2356";}} @media (min-height:2357px){#D:after{content:" x 2357";}} @media (min-height:2358px){#D:after{content:" x 2358";}} @media (min-height:2359px){#D:after{content:" x 2359";}} @media (min-height:2360px){#D:after{content:" x 2360";}} @media (min-height:2361px){#D:after{content:" x 2361";}} @media (min-height:2362px){#D:after{content:" x 2362";}} @media (min-height:2363px){#D:after{content:" x 2363";}} @media (min-height:2364px){#D:after{content:" x 2364";}} @media (min-height:2365px){#D:after{content:" x 2365";}} @media (min-height:2366px){#D:after{content:" x 2366";}} @media (min-height:2367px){#D:after{content:" x 2367";}} @media (min-height:2368px){#D:after{content:" x 2368";}} @media (min-height:2369px){#D:after{content:" x 2369";}} @media (min-height:2370px){#D:after{content:" x 2370";}} @media (min-height:2371px){#D:after{content:" x 2371";}} @media (min-height:2372px){#D:after{content:" x 2372";}} @media (min-height:2373px){#D:after{content:" x 2373";}} @media (min-height:2374px){#D:after{content:" x 2374";}} @media (min-height:2375px){#D:after{content:" x 2375";}} @media (min-height:2376px){#D:after{content:" x 2376";}} @media (min-height:2377px){#D:after{content:" x 2377";}} @media (min-height:2378px){#D:after{content:" x 2378";}} @media (min-height:2379px){#D:after{content:" x 2379";}} @media (min-height:2380px){#D:after{content:" x 2380";}} @media (min-height:2381px){#D:after{content:" x 2381";}} @media (min-height:2382px){#D:after{content:" x 2382";}} @media (min-height:2383px){#D:after{content:" x 2383";}} @media (min-height:2384px){#D:after{content:" x 2384";}} @media (min-height:2385px){#D:after{content:" x 2385";}} @media (min-height:2386px){#D:after{content:" x 2386";}} @media (min-height:2387px){#D:after{content:" x 2387";}} @media (min-height:2388px){#D:after{content:" x 2388";}} @media (min-height:2389px){#D:after{content:" x 2389";}} @media (min-height:2390px){#D:after{content:" x 2390";}} @media (min-height:2391px){#D:after{content:" x 2391";}} @media (min-height:2392px){#D:after{content:" x 2392";}} @media (min-height:2393px){#D:after{content:" x 2393";}} @media (min-height:2394px){#D:after{content:" x 2394";}} @media (min-height:2395px){#D:after{content:" x 2395";}} @media (min-height:2396px){#D:after{content:" x 2396";}} @media (min-height:2397px){#D:after{content:" x 2397";}} @media (min-height:2398px){#D:after{content:" x 2398";}} @media (min-height:2399px){#D:after{content:" x 2399";}} @media (min-height:2400px){#D:after{content:" x 2400";}} @media (min-height:2401px){#D:after{content:" x 2401";}} @media (min-height:2402px){#D:after{content:" x 2402";}} @media (min-height:2403px){#D:after{content:" x 2403";}} @media (min-height:2404px){#D:after{content:" x 2404";}} @media (min-height:2405px){#D:after{content:" x 2405";}} @media (min-height:2406px){#D:after{content:" x 2406";}} @media (min-height:2407px){#D:after{content:" x 2407";}} @media (min-height:2408px){#D:after{content:" x 2408";}} @media (min-height:2409px){#D:after{content:" x 2409";}} @media (min-height:2410px){#D:after{content:" x 2410";}} @media (min-height:2411px){#D:after{content:" x 2411";}} @media (min-height:2412px){#D:after{content:" x 2412";}} @media (min-height:2413px){#D:after{content:" x 2413";}} @media (min-height:2414px){#D:after{content:" x 2414";}} @media (min-height:2415px){#D:after{content:" x 2415";}} @media (min-height:2416px){#D:after{content:" x 2416";}} @media (min-height:2417px){#D:after{content:" x 2417";}} @media (min-height:2418px){#D:after{content:" x 2418";}} @media (min-height:2419px){#D:after{content:" x 2419";}} @media (min-height:2420px){#D:after{content:" x 2420";}} @media (min-height:2421px){#D:after{content:" x 2421";}} @media (min-height:2422px){#D:after{content:" x 2422";}} @media (min-height:2423px){#D:after{content:" x 2423";}} @media (min-height:2424px){#D:after{content:" x 2424";}} @media (min-height:2425px){#D:after{content:" x 2425";}} @media (min-height:2426px){#D:after{content:" x 2426";}} @media (min-height:2427px){#D:after{content:" x 2427";}} @media (min-height:2428px){#D:after{content:" x 2428";}} @media (min-height:2429px){#D:after{content:" x 2429";}} @media (min-height:2430px){#D:after{content:" x 2430";}} @media (min-height:2431px){#D:after{content:" x 2431";}} @media (min-height:2432px){#D:after{content:" x 2432";}} @media (min-height:2433px){#D:after{content:" x 2433";}} @media (min-height:2434px){#D:after{content:" x 2434";}} @media (min-height:2435px){#D:after{content:" x 2435";}} @media (min-height:2436px){#D:after{content:" x 2436";}} @media (min-height:2437px){#D:after{content:" x 2437";}} @media (min-height:2438px){#D:after{content:" x 2438";}} @media (min-height:2439px){#D:after{content:" x 2439";}} @media (min-height:2440px){#D:after{content:" x 2440";}} @media (min-height:2441px){#D:after{content:" x 2441";}} @media (min-height:2442px){#D:after{content:" x 2442";}} @media (min-height:2443px){#D:after{content:" x 2443";}} @media (min-height:2444px){#D:after{content:" x 2444";}} @media (min-height:2445px){#D:after{content:" x 2445";}} @media (min-height:2446px){#D:after{content:" x 2446";}} @media (min-height:2447px){#D:after{content:" x 2447";}} @media (min-height:2448px){#D:after{content:" x 2448";}} @media (min-height:2449px){#D:after{content:" x 2449";}} @media (min-height:2450px){#D:after{content:" x 2450";}} @media (min-height:2451px){#D:after{content:" x 2451";}} @media (min-height:2452px){#D:after{content:" x 2452";}} @media (min-height:2453px){#D:after{content:" x 2453";}} @media (min-height:2454px){#D:after{content:" x 2454";}} @media (min-height:2455px){#D:after{content:" x 2455";}} @media (min-height:2456px){#D:after{content:" x 2456";}} @media (min-height:2457px){#D:after{content:" x 2457";}} @media (min-height:2458px){#D:after{content:" x 2458";}} @media (min-height:2459px){#D:after{content:" x 2459";}} @media (min-height:2460px){#D:after{content:" x 2460";}} @media (min-height:2461px){#D:after{content:" x 2461";}} @media (min-height:2462px){#D:after{content:" x 2462";}} @media (min-height:2463px){#D:after{content:" x 2463";}} @media (min-height:2464px){#D:after{content:" x 2464";}} @media (min-height:2465px){#D:after{content:" x 2465";}} @media (min-height:2466px){#D:after{content:" x 2466";}} @media (min-height:2467px){#D:after{content:" x 2467";}} @media (min-height:2468px){#D:after{content:" x 2468";}} @media (min-height:2469px){#D:after{content:" x 2469";}} @media (min-height:2470px){#D:after{content:" x 2470";}} @media (min-height:2471px){#D:after{content:" x 2471";}} @media (min-height:2472px){#D:after{content:" x 2472";}} @media (min-height:2473px){#D:after{content:" x 2473";}} @media (min-height:2474px){#D:after{content:" x 2474";}} @media (min-height:2475px){#D:after{content:" x 2475";}} @media (min-height:2476px){#D:after{content:" x 2476";}} @media (min-height:2477px){#D:after{content:" x 2477";}} @media (min-height:2478px){#D:after{content:" x 2478";}} @media (min-height:2479px){#D:after{content:" x 2479";}} @media (min-height:2480px){#D:after{content:" x 2480";}} @media (min-height:2481px){#D:after{content:" x 2481";}} @media (min-height:2482px){#D:after{content:" x 2482";}} @media (min-height:2483px){#D:after{content:" x 2483";}} @media (min-height:2484px){#D:after{content:" x 2484";}} @media (min-height:2485px){#D:after{content:" x 2485";}} @media (min-height:2486px){#D:after{content:" x 2486";}} @media (min-height:2487px){#D:after{content:" x 2487";}} @media (min-height:2488px){#D:after{content:" x 2488";}} @media (min-height:2489px){#D:after{content:" x 2489";}} @media (min-height:2490px){#D:after{content:" x 2490";}} @media (min-height:2491px){#D:after{content:" x 2491";}} @media (min-height:2492px){#D:after{content:" x 2492";}} @media (min-height:2493px){#D:after{content:" x 2493";}} @media (min-height:2494px){#D:after{content:" x 2494";}} @media (min-height:2495px){#D:after{content:" x 2495";}} @media (min-height:2496px){#D:after{content:" x 2496";}} @media (min-height:2497px){#D:after{content:" x 2497";}} @media (min-height:2498px){#D:after{content:" x 2498";}} @media (min-height:2499px){#D:after{content:" x 2499";}} @media (min-height:2500px){#D:after{content:" x 2500";}} @media (min-height:2501px){#D:after{content:" x 2501";}} @media (min-height:2502px){#D:after{content:" x 2502";}} @media (min-height:2503px){#D:after{content:" x 2503";}} @media (min-height:2504px){#D:after{content:" x 2504";}} @media (min-height:2505px){#D:after{content:" x 2505";}} @media (min-height:2506px){#D:after{content:" x 2506";}} @media (min-height:2507px){#D:after{content:" x 2507";}} @media (min-height:2508px){#D:after{content:" x 2508";}} @media (min-height:2509px){#D:after{content:" x 2509";}} @media (min-height:2510px){#D:after{content:" x 2510";}} @media (min-height:2511px){#D:after{content:" x 2511";}} @media (min-height:2512px){#D:after{content:" x 2512";}} @media (min-height:2513px){#D:after{content:" x 2513";}} @media (min-height:2514px){#D:after{content:" x 2514";}} @media (min-height:2515px){#D:after{content:" x 2515";}} @media (min-height:2516px){#D:after{content:" x 2516";}} @media (min-height:2517px){#D:after{content:" x 2517";}} @media (min-height:2518px){#D:after{content:" x 2518";}} @media (min-height:2519px){#D:after{content:" x 2519";}} @media (min-height:2520px){#D:after{content:" x 2520";}} @media (min-height:2521px){#D:after{content:" x 2521";}} @media (min-height:2522px){#D:after{content:" x 2522";}} @media (min-height:2523px){#D:after{content:" x 2523";}} @media (min-height:2524px){#D:after{content:" x 2524";}} @media (min-height:2525px){#D:after{content:" x 2525";}} @media (min-height:2526px){#D:after{content:" x 2526";}} @media (min-height:2527px){#D:after{content:" x 2527";}} @media (min-height:2528px){#D:after{content:" x 2528";}} @media (min-height:2529px){#D:after{content:" x 2529";}} @media (min-height:2530px){#D:after{content:" x 2530";}} @media (min-height:2531px){#D:after{content:" x 2531";}} @media (min-height:2532px){#D:after{content:" x 2532";}} @media (min-height:2533px){#D:after{content:" x 2533";}} @media (min-height:2534px){#D:after{content:" x 2534";}} @media (min-height:2535px){#D:after{content:" x 2535";}} @media (min-height:2536px){#D:after{content:" x 2536";}} @media (min-height:2537px){#D:after{content:" x 2537";}} @media (min-height:2538px){#D:after{content:" x 2538";}} @media (min-height:2539px){#D:after{content:" x 2539";}} @media (min-height:2540px){#D:after{content:" x 2540";}} @media (min-height:2541px){#D:after{content:" x 2541";}} @media (min-height:2542px){#D:after{content:" x 2542";}} @media (min-height:2543px){#D:after{content:" x 2543";}} @media (min-height:2544px){#D:after{content:" x 2544";}} @media (min-height:2545px){#D:after{content:" x 2545";}} @media (min-height:2546px){#D:after{content:" x 2546";}} @media (min-height:2547px){#D:after{content:" x 2547";}} @media (min-height:2548px){#D:after{content:" x 2548";}} @media (min-height:2549px){#D:after{content:" x 2549";}} @media (min-height:2550px){#D:after{content:" x 2550";}} @media (min-height:2551px){#D:after{content:" x 2551";}} @media (min-height:2552px){#D:after{content:" x 2552";}} @media (min-height:2553px){#D:after{content:" x 2553";}} @media (min-height:2554px){#D:after{content:" x 2554";}} @media (min-height:2555px){#D:after{content:" x 2555";}} @media (min-height:2556px){#D:after{content:" x 2556";}} @media (min-height:2557px){#D:after{content:" x 2557";}} @media (min-height:2558px){#D:after{content:" x 2558";}} @media (min-height:2559px){#D:after{content:" x 2559";}} @media (min-height:2560px){#D:after{content:" x 2560";}} @media (min-height:2561px){#D:after{content:"";}} ================================================ FILE: index.html ================================================ tzp index

TorZillaPrint

take me to the main test
index
GITHUB
repocome and say hi
SCREEN
screen iframe test: parent vs iframe measurements
screen orientation test: orientation, aspect ratio, angles and rotation
new window sizes a what if sim for RFP's new win sizes
FEATURE DETECTION
chrome test: chrome:// + resource:// files
engine ... you can run but you can't hide
engine properties test: enumeration of engine properties
os test: gecko os detection logic
versions a history of version pocs tzp uses or considered
REGION
app language application language tests
intl collation
datetimeformat components | date-&-timestyle | dayperiod | listformat | relatedyear | timezonename
displaynames calendar | currency | datetimefield | language | region | script
durationformat
numberformat compactdisplay | currencydisplay | formattoparts | notation | signdisplay | unitdisplay
pluralrules select | selectrange
relativetimeformat
resolvedoptions
supportedlocales test: supportedLocalesOf
supportedvalues test: supportedValuesOf
timezones proof: min dates for max results
COOKIES & STORAGE
sanitizing ... with zombies!
DEVICES
recursion test: recursion levels and stack lengths
scrolling test: smooth scrolling
pointer event test: pointer and touch events
CANVAS
canvas spoof canvas spoof detection
canvas noise canvas spoof fingerprinting
canvas rfp test: RFP random canvas characteristics
FONTS
bridge-moji test: accessibilty, looks & feel
font async test: async font fallback
font debug test: full check on any font ... down the rabbit hole
mac fonts analysis: mac font diffs per major release
script defaults test: script default proportional font and sizes
scripts test: script support vs. font visibility
system fonts test: system font exposure
script view view: generic font-families per script
CODECS
codecs test: canPlayType & isTypeSupported
CSS
css colors view: css colors
ELEMENTS
domrect domrect spoof detection
[ratio] domrect domrect spoof detection
element keys test: element properties
element sizes fonts | forms | other
MISC
functions ... just scraping properties
math research: likely math functions and polyfills
math data research: ongoing results
math spoof math spoof detection
OTHER
reader view test: generic test page
window.name check window.name clearance per eTLD+1

================================================ FILE: js/audio.js ================================================ 'use strict'; /* code based on https://canvasblocker.kkapsner.de/test/ https://audiofingerprint.openwpm.com/ */ function byteArrayToHex(arrayBuffer){ var chunks = []; (new Uint32Array(arrayBuffer)).forEach(function(num){ chunks.push(num.toString(16)); }); return chunks.map(function(chunk){ return '0'.repeat(8 - chunk.length) + chunk; }).join(''); } function check_audioLies() { const audioList = [ 'AnalyserNode.getByteFrequencyData','AnalyserNode.getByteTimeDomainData', 'AnalyserNode.getFloatFrequencyData','AnalyserNode.getFloatTimeDomainData', 'AudioBuffer.copyFromChannel','AudioBuffer.getChannelData', 'BiquadFilterNode.getFrequencyResponse', ] if (runSL) {addProxyLie('AudioBuffer.copyFromChannel')} return audioList.some(lie => sData[SECT99].indexOf(lie) >= 0) } const get_audio_context = (METRIC) => new Promise(resolve => { let t0 = nowFn() let hash, btn ='', data = {}, notation = rfp_red, isLies = false try { // unsorted function a(a, b, c) { for (let d in b) 'dopplerFactor' === d || 'speedOfSound' === d || 'currentTime' === d || 'number' !== typeof b[d] && 'string' !== typeof b[d] || (a[(c ? c : '') + d] = b[d]) return a } let f = new window.AudioContext let obj let d = f.createAnalyser() obj = a({}, f, 'ac-') obj = a(obj, f.destination, 'ac-') obj = a(obj, f.listener, 'ac-') obj = a(obj, d, 'an-') // sort, type check etc if (runST) {obj['ac-channelCount'] = '4'; obj['an-fftSize'] = null // change type: this will trigger isLies } else if (runSL) { obj['ac-channelCount'] = 4} // change expected value let oHardcoded = {} // FF70+: keys [20] + expected hardcoded values [16] let hardcodeExclude = ['ac-outputLatency','ac-sampleRate','ac-maxChannelCount','an-channelCount'] let numberExclude = [ 'ac-channelCountMode','ac-channelInterpretation','ac-state','an-channelCountMode', 'an-channelInterpretation','ac-sinkId' ] for (const k of Object.keys(obj).sort()) { data[k] = obj[k] oHardcoded[k] = hardcodeExclude.includes(k) ? '' : obj[k] // regardless of hardcoded check, catch all type check entropy let typeCheck = typeFn(obj[k]) let typeMatch = numberExclude.includes(k) ? ('ac-sinkId' == k ? 'empty string' : 'string') : 'number' if (typeMatch !== typeCheck) { log_error(11, METRIC +'_'+ k, zErrType + typeCheck) if (!isSmart) {data[k] = zErr} // non smart reflect error in data isLies = true // otherwise smart uses isLies and returns untrustworthy } } if (mini(oHardcoded) !== 'dfda7813') {isLies = true} // ac-state changes in blink (IDK about webkit ToDo I guess) on a re-run // gRun = suspended, reruns = running // doesn't seem partically useful, so let's change it in non-gecko if (!isGecko) { if (undefined !== data['ac-state']) {data['ac-state'] = zNA} } // notate // non-RFP outputLatency can be variable per tab/run - return n/a + rehash to avoid any noise hash = mini(data); btn = addButton(11, METRIC, Object.keys(data).length +' keys') if (isOS !== undefined) { if ('windows' == isOS && '67a3eeee' == hash) {notation = rfp_green // 0.04 } else if ('mac' == isOS && 'debdefc0' == hash) {notation = rfp_green // 512/44100 (RFP hardcodes latency) } else if ('android' == isOS && '2b9d44b0' == hash) {notation = rfp_green // 0.02 } else if ('9b69969b' == hash) {notation = rfp_green} // 0.025 catchall incl linux } if (isGecko && notation !== rfp_green) { notation += ' [latency: '+ data['ac-outputLatency'] +']' data['ac-outputLatency'] = zNA; hash = mini(data) } } catch(e) { hash = log_error(11, METRIC, e); data = zErr } addBoth(11, METRIC, hash, btn, notation, data, isLies) log_perf(11, METRIC, t0) return resolve() }) const get_audio_offline = (METRIC) => new Promise(resolve => { let t0 = nowFn(), notation = rfp_red, isLies = false function outputErrors(display) { addBoth(11, METRIC, display,'', notation, zErr) return resolve() } // ToDo: maybe reduce bufferLen as long as it doesn't change entropy // also: when we add RFP + math PoC we need only check for protection (like canvas) try { if (runSE) {foo++} const bufferLen = 5000 // 5000 to match documented const context = new window.OfflineAudioContext(1, bufferLen, 44100) const dynamicsCompressor = context.createDynamicsCompressor() // servo breaks here const oscillator = context.createOscillator() // set oscillator.type = 'triangle' oscillator.frequency.value = 10000 dynamicsCompressor.threshold && (dynamicsCompressor.threshold.value = -50) dynamicsCompressor.knee && (dynamicsCompressor.knee.value = 40) dynamicsCompressor.attack && (dynamicsCompressor.attack.value = 0) dynamicsCompressor.ratio && (dynamicsCompressor.ratio.value = 12) dynamicsCompressor.reduction && (dynamicsCompressor.reduction.value = -20) // does this do anything dynamicsCompressor.release && (dynamicsCompressor.release.value = .25) // connect dynamicsCompressor.connect(context.destination) oscillator.connect(dynamicsCompressor) // start oscillator.start(0) context.startRendering() context.oncomplete = function(event) { try { dynamicsCompressor.disconnect() let copyTest = new Float32Array(bufferLen) event.renderedBuffer.copyFromChannel(copyTest, 0) // JSShelter errors here let getTest = event.renderedBuffer.getChannelData(0) // JSShelter errors here Promise.all([ crypto.subtle.digest('SHA-1', getTest), crypto.subtle.digest('SHA-1', copyTest), ]).then(function(hashes){ // sum let sum = 0 for (let i=0; i < copyTest.length; i++) { let x = copyTest[i] if (i > (bufferLen-501) && i < bufferLen) {sum += Math.abs(x)} } // get/copy let hashC = mini(byteArrayToHex(hashes[1])) let hashG = mini(byteArrayToHex(hashes[0])) // lies let isSame = hashG == hashC, display, btn = addButton(11, METRIC +'_data') if (!isSame) { isLies = true addDetail(METRIC +'_data', {'copyFromChannel': copyTest, 'getChannelData': getTest}) display = 'mixed' } else { // no need to list twice isLies = check_audioLies() addDetail(METRIC +'_data', copyTest) display = hashC btn += ' '+ sum } // notation: three results since 1877221 FF124+ split x86 into 32/64 bitness // isArch: true = large arrays else it's an error string if (true === isArch) { if ('a7c1fbb6' == hashC) {notation = sgtick+'x86_64/amd_64]'+sc } else if ('a34c73cd' == hashC) {notation = sgtick+'ARM64/aarch64]'+sc} } else { if ('24fc63ce' == hashC) {notation = sgtick+'x86/i686/ARMv7]'+sc} } addData(11, METRIC, display,'', isLies) addDisplay(11, METRIC, display, btn, notation, isLies) log_perf(11, METRIC, t0) return resolve() }) .catch(function(e){ outputErrors(log_error(11, METRIC, e)) }) } catch(e) { outputErrors(log_error(11, METRIC, e)) } } } catch(e) { try { if (gRun) {dom.audio_test_oscillator_compressor = zNA; dom.audio_test_oscillator = zNA; dom.audio_test = zNA} } catch {} outputErrors(log_error(11, METRIC, e)) } }) const outputAudio = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('audio')) {return resolve()} Promise.all([ get_audio_context('audioContext'), get_audio_offline('offlineAudioContext'), ]).then(function(){ return resolve() }) }) countJS(11) ================================================ FILE: js/canvas.js ================================================ 'use strict'; /* outputCanvas() based on https://canvasblocker.kkapsner.de/test/ */ function check_canvas_to(data) { // only called if per-execution let len = data.length if (![166,170,174,178].includes(len)) {return false} let slice1 = data.slice(72,80) if ('lEQVQoU2' == slice1) { let slice2 = data.slice(data.length - 10, data.length) if ('VORK5CYII=' == slice2 || '5ErkJggg==' == slice2 || 'lFTkSuQmCC' == slice2) { return true // RFP } } return false } const get_canvas = () => new Promise(resolve => { const sizeW = 16, sizeH = 8, pixelcount = sizeW * sizeH, allZeros = '93bd94c5' // FF95+: compression 1724331 / 1737038 const oKnown = { 'ge_white': ['d5f8f171'], 'isPointInPath': ['db0e3f08'], 'isPointInStroke': ['a77e328a'], 'to_white': ['35e41537'], 'toBlob': ['3afc375a'], 'toBlob_solid': ['56ea6104'], 'toDataURL': ['3afc375a'], 'toDataURL_solid': ['56ea6104'], } // libz-rs // FF137 1910796: Enable libz-rs on nightly: this changes our known hashes // FF139 1949947: Upgrade zlib-rs/libz-rs-sys to 0.4.2. (new to*_solids) if (isVer > 136) { oKnown['toBlob'].push('e328ec8e') oKnown['toBlob_solid'].push('9d0b9932','cfd52a1f') oKnown['toDataURL'].push('e328ec8e') oKnown['toDataURL_solid'].push('9d0b9932','cfd52a1f') oKnown['to_white'].push('3e72d1fd') } let isCanvasGet ='', isCanvasGetChannels ='', isGetStealth = false function check_canvas_get(dataname, runNo) { let data = oData[dataname] let dataDrawn = oDataDrawn[dataname] let isMatch = mini(dataDrawn) == mini(data) // run1 return if a match or not if (runNo == 1) {return isMatch} // run2 quick exit: return skip if nothing to do if (isMatch) {return 'skip'} // run2 otherwise return if RFP-like and create strings let aDrawn = [], aRead = [], indexChanged = [] let altP = 0, altR = 0, altG = 0, altB = 0, altA = 0, altAll = 0 for (let x=0; x < pixelcount; x++) { let k = x * 4 aDrawn = dataDrawn.slice(k, k+4) aRead = data.slice(k, k+4) if (aDrawn.join() !== aRead.join()) { // pixels altP++ indexChanged.push(k) } if (aDrawn[0] !== aRead[0]) { altR++} // channels if (aDrawn[1] !== aRead[1]) { altG++} if (aDrawn[2] !== aRead[2]) { altB++} if (aDrawn[3] !== aRead[3]) { altA++} // ToDo: range: worth it? } // stealth check: anything in changed not in font let aNotInFonts = indexChanged.filter(x => !indexFont.includes(x)) isGetStealth = aNotInFonts.length == 0 // noise FP let strFP ='', aNote = [] aNote.push('p'+ Math.floor((altP / pixelcount) * 100)) if (altR > 0) {strFP += 'r'; aNote.push('r'+Math.floor((altR / pixelcount) * 100))} if (altG > 0) {strFP += 'g'; aNote.push('g'+ Math.floor((altG / pixelcount) * 100))} if (altB > 0) {strFP += 'b'; aNote.push('b'+ Math.floor((altB / pixelcount) * 100))} if (altA > 0) {strFP += 'a'; aNote.push('a'+ Math.floor((altA / pixelcount) * 100))} // FP data isCanvasGetChannels = (isGetStealth ? 'stealth | ' : '') + strFP // display data: keep android short if (isDesktop) {isCanvasGet = ' ['+ (isGetStealth ? 'stealth ' : '') +'%: '+ aNote.join(' ') +']' } else if (isGetStealth) {isCanvasGet = ' [stealth]'} // pixels: allow 2 collision if (altP < (pixelcount - 2)) {return false} // rgb: ran 100k tests: lowest 124/128: allow 8 collsions // with a solid, collisions are amplified: 112/128 seems to be the lowest given the pattern repeats let maxCollisions = 'getImageData_solid' == dataname ? 24 : 8 if (altR < (pixelcount - maxCollisions)) {return false} if (altG < (pixelcount - maxCollisions)) {return false} if (altB < (pixelcount - maxCollisions)) {return false} // alpha: not randomized: higher collisons: lowest 96/128: allow 33% if ((altA / pixelcount) < .66) {return false} return true // RFP traits } var known = { createHashes: function(window, runNo){ let outputs = [ { class: window.CanvasRenderingContext2D, name: 'getImageData', value: function(){ const METRIC = 'getImageData' if (aSkip.includes(METRIC)) {return 'skip'} try { var context = getKnownGet() let imageData = context.getImageData(0,0, sizeW, sizeH) if (runST) {imageData = null} else if (runSI) {imageData = {}} if ('object' !== typeFn(imageData, true)) {throw zErrType + typeFn(imageData)} let expected = '[object ImageData]' if (imageData+'' !== expected) {throw zErrInvalid +'expected '+ expected +': got '+ imageData+''} oData[METRIC] = imageData.data return mini(imageData.data) } catch(e) { oErrors[METRIC] = e+'' return zErr } } }, { class: window.CanvasRenderingContext2D, name: 'getImageData_solid', value: function(){ const METRIC = 'getImageData_solid' if (aSkip.includes(METRIC)) {return 'skip'} try { var context = getKnownGetSolid() let imageData = context.getImageData(0,0, sizeW, sizeH) if (runST) {imageData = null} else if (runSI) {imageData = {}} if ('object' !== typeFn(imageData, true)) {throw zErrType + typeFn(imageData)} let expected = '[object ImageData]' if (imageData+'' !== expected) {throw zErrInvalid +'expected '+ expected +': got '+ imageData+''} oData[METRIC] = imageData.data return mini(imageData.data) } catch(e) { oErrors[METRIC] = e+'' return zErr } } }, { class: window.CanvasRenderingContext2D, name: 'isPointInPath', value: function(){ const METRIC = 'isPointInPath' if (aSkip.includes(METRIC)) {return 'skip'} try { var context = getKnownPath() var data = new Uint8Array(sizeW * sizeH) var dataR = context.isPointInPath(0, 0) if (runST) {dataR = 0} let typeCheck = typeFn(dataR) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} for (let x = 0; x < sizeW; x++){ for (let y = 0; y < sizeH; y++){ data[y * sizeW + x] = context.isPointInPath(x, y) } } data = data.join('') oData[METRIC] = data return mini(data) } catch(e) { oErrors[METRIC] = e+'' return zErr } } }, { class: window.CanvasRenderingContext2D, name: 'isPointInStroke', value: function(){ const METRIC = 'isPointInStroke' if (aSkip.includes(METRIC)) {return 'skip'} try { let context = getKnownPath() var data = new Uint8Array(sizeW * sizeH) var dataR = context.isPointInStroke(0, 0) if (runST) {dataR = 'false'} let typeCheck = typeFn(dataR) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} for (let x = 0; x < sizeW; x++){ for (let y = 0; y < sizeH; y++){ data[y * sizeW + x] = context.isPointInStroke(x, y) } } data = data.join('') oData[METRIC] = data return mini(data) } catch(e) { oErrors[METRIC] = e+'' return zErr } } }, { name: 'toBlob', value: function(){ return new Promise(function(resolve, reject){ const METRIC = 'toBlob' if (aSkip.includes(METRIC)) {resolve('skip')} try { var timeout = window.setTimeout(function(){ oErrors[METRIC] = zErrTime resolve(zErrTime) }, 750) if (!runTE) { getKnownTo().canvas.toBlob(function(blob){ window.clearTimeout(timeout) var reader = new FileReader() reader.onload = function(){ let value = reader.result if (runST) {value =''} let typeCheck = typeFn(value) if ('string' === typeCheck ) { oData[METRIC] = value resolve(mini(reader.result)) } else { oErrors[METRIC] = zErrType + typeCheck resolve(zErr) } } reader.onerror = function(){ oErrors[METRIC] = zErr +' undefined [.onerror]' reject(zErr) } reader.readAsDataURL(blob) }) } } catch(e) { oErrors[METRIC] = e+'' resolve(zErr) } }) } }, { name: 'toBlob_solid', value: function(){ return new Promise(function(resolve, reject){ const METRIC = 'toBlob_solid' if (aSkip.includes(METRIC)) {resolve('skip')} try { var timeout = window.setTimeout(function(){ oErrors[METRIC] = zErrTime resolve(zErrTime) }, 750) if (!runTE) { getKnownToSolid().canvas.toBlob(function(blob){ window.clearTimeout(timeout) var reader = new FileReader() reader.onload = function(){ let value = reader.result if (runST) {value =''} let typeCheck = typeFn(value) if ('string' === typeCheck ) { oData[METRIC] = value resolve(mini(reader.result)) } else { oErrors[METRIC] = zErrType + typeCheck resolve(zErr) } } reader.onerror = function(){ oErrors[METRIC] = zErr +' undefined [.onerror]' reject(zErr) } reader.readAsDataURL(blob) }) } } catch(e) { oErrors[METRIC] = e+'' resolve(zErr) } }) } }, { name: 'toDataURL', value: function(){ let METRIC = 'toDataURL' if (aSkip.includes(METRIC)) {return 'skip'} try { let data = getKnownTo().canvas.toDataURL() if (runST) {data = undefined} let typeCheck = typeFn(data) if ('string' !== typeCheck) {throw zErrType + typeCheck} oData[METRIC] = data return mini(data) } catch(e) { oErrors[METRIC] = e+'' return zErr } } }, { name: 'toDataURL_solid', value: function(){ let METRIC = 'toDataURL_solid' if (aSkip.includes(METRIC)) {return 'skip'} try { let data = getKnownToSolid().canvas.toDataURL() if (runST) {data = undefined} let typeCheck = typeFn(data) if ('string' !== typeCheck) {throw zErrType + typeCheck} oData[METRIC] = data return mini(data) } catch(e) { oErrors[METRIC] = e+'' return zErr } } }, ]; function isSupported(output){ let key = output.name if (key.includes('_solid')) {key = key.slice(0,-6)} return !!(output.class? output.class: window.HTMLCanvasElement).prototype[key] } function getKnownTo(){ let canvas = dom.tzpCanvasTo let ctx = canvas.getContext('2d') if (oDrawn['to']) {return ctx} // color the background ctx.fillStyle = 'rgba('+ solidPink +')' ctx.fillRect(0, 0, sizeW, sizeH) // trigger fillText stealth let fpText = '\u2588\u2588\u2588\u2588' // full block ctx.font = '512px sans-serif' // large ctx.textBaseline = 'top' ctx.textBaseline = 'alphabetic' ctx.fillText(fpText,0,0) for (let x = 0; x < sizeW; x++) { let xEven = (x % 2 == 0) for (let y = 0; y < sizeH; y++) { let yEven = (y % 2 == 0) let isRandom = (xEven + yEven == 1 || xEven + yEven == 2) // 3/4ths if (isRandom) { ctx.fillStyle = 'rgba('+ (x*y) +','+ (x * 16) +','+ (y * 16) +',255)' ctx.fillRect(x, y, 1, 1) } } } oDrawn['to'] = true return ctx } function getKnownToSolid(){ let canvas = dom.tzpCanvasToSolid let ctx = canvas.getContext('2d') if (oDrawn['to_solid']) {return ctx} ctx.fillStyle = 'rgba('+ solidPink +')' ctx.fillRect(0, 0, sizeW, sizeH) oDrawn['to_solid'] = true return ctx } function getKnownGet(){ let canvas = dom.tzpCanvasGet let ctx = canvas.getContext('2d') if (oDrawn['get']) {return ctx} // color the background ctx.fillStyle = 'rgba('+ solidClrs +')' ctx.fillRect(0, 0, sizeW, sizeH) // trigger fillText stealth: try to cover every pixel let fpText = '\u2588\u2588\u2588\u2588' // full block ctx.font = '512px sans-serif' // large ctx.textBaseline = 'top' ctx.textBaseline = 'alphabetic' ctx.fillText(fpText,0,0) /* // trigger strokeText stealth // don't overwrite all the fillText // see PoC notes: too risky fpText = '-' ctx.font = '16px monospace' ctx.strokeStyle ='rgba('+ solidClrs +')' for (let x=0; x < sizeW/2; x++) { for (let y=0; y < sizeH/2; y++) {ctx.strokeText(fpText,x,y)} } //*/ // now color the rest with our random colors // swap x/y loop order to match getImageData uint let ignore = 'rgba('+ solidClrs +')' for (let y=0; y < sizeH; y++) { for (let x=0; x < sizeW; x++) { let style = dataToDraw[(y * sizeW) + x] if (style !== ignore) { ctx.fillStyle = style ctx.fillRect(x, y, 1, 1) } } } oDrawn['get'] = true return ctx } function getKnownGetSolid(){ let canvas = dom.tzpCanvasGetSolid let ctx = canvas.getContext('2d') if (oDrawn['get_solid']) {return ctx} ctx.fillStyle = 'rgba('+ solidClrs +')' ctx.fillRect(0, 0, sizeW, sizeH) oDrawn['get_solid'] = true return ctx } function getKnownPath(){ let ctx = dom.tzpCanvasPath.getContext('2d') if (oDrawn['path']) {return ctx} ctx.fillStyle = 'rgba(255,255,255,255)' ctx.beginPath() ctx.rect(2,5,8,7) ctx.closePath() ctx.fill() oDrawn['path'] = true return ctx } var finished = Promise.all(outputs.map(function(output){ return new Promise(function(resolve, reject){ var displayValue try { var supported = output.supported? output.supported(): isSupported(output); if (supported){ displayValue = output.value() } else { oErrors[output.name] = zErr displayValue = zErr } } catch(e) { oErrors[output.name] = e+'' displayValue = zErr } Promise.resolve(displayValue).then(function(displayValue){ output.displayValue = displayValue resolve(output) }, function(e){ oErrors[output.name] = e+'' output.displayValue = zErr resolve(zErr) }) }) })) return finished } } // oDrawn: only draw the canvas once per runNo // if input is faked, it would also be faked the second time let oDrawn = {'get': false, 'get_solid': false, 'path': false, 'to': false, 'to_solid': false} let oRes = {}, oFP = {}, oErrors = {}, oData = {}, aSkip = [], countFake = 0 let solidPink = '224,33,138,255' // go Barbie! // random getImageData let tmpDrawn = new Uint8ClampedArray(sizeW * sizeH * 4) let tmpSolid = new Uint8ClampedArray(sizeW * sizeH * 4) let dataToDraw = [], indexFont = [] let solidR = Math.floor(Math.random()*255), solidG = Math.floor(Math.random()*255), solidB = Math.floor(Math.random()*255) let solidClrs = solidR +','+ solidG +','+ solidB +',255' let counter = -1 for (let x=0; x < sizeW; x++) { let xEven = (x % 2 == 0) for (let y=0; y < sizeH; y++) { counter ++ let k = counter * 4 let yEven = (y % 2 == 0) // xEven + yEven == 1 = checkerboard = 1/2 // xEven + yEven == 2 = another 1/4 // xEven + yEven == 0 = the remainder: of which we can further reduce e.g. multples of 3 let isRandom = (xEven + yEven == 1 || xEven + yEven == 2) // 3/4ths if (!isRandom) { if ((x * y) % 3 == 0 ) {isRandom = true} // brings us to 113/128 } if (isRandom) { // random: 113 let valueR = Math.floor(Math.random()*255), valueG = Math.floor(Math.random()*255), valueB = Math.floor(Math.random()*255) tmpDrawn[k] = valueR tmpDrawn[k+1] = valueG tmpDrawn[k+2] = valueB tmpDrawn[k+3] = 255 dataToDraw.push('rgba('+ valueR +','+ valueG +','+ valueB +',255)') } else { indexFont.push(k) // solid: 15 tmpDrawn[k] = solidR tmpDrawn[k+1] = solidG tmpDrawn[k+2] = solidB tmpDrawn[k+3] = 255 dataToDraw.push('rgba('+ solidClrs +')') } // solid tmpSolid[k] = solidR tmpSolid[k+1] = solidG tmpSolid[k+2] = solidB tmpSolid[k+3] = 255 } } let oDataDrawn = {'getImageData': tmpDrawn, 'getImageData_solid': tmpSolid} // ensure sizes let aCanvas = ['Get','GetSolid','Path','To','ToSolid'] aCanvas.forEach(function(k){let el = dom['tzpCanvas'+ k]; el.width = sizeW; el.height = sizeH}) function exit() { console.debug(oData) for (const m of Object.keys(oFP)) { addBoth(9, m, oFP[m].value, '', oFP[m].notation, oFP[m].data) } return resolve() } Promise.all([ known.createHashes(window, 1) ]).then(function(run1){ // ToDo: learn more about PNG file structure and detect this more robustly let aChunk = [ 'ABBkZUJH', // initial analysis 'AAQZGVCR', // this comes up in solid FF145 but not FF146+ ] //aChunk = ['5ErkJggg==','VORK5CYII='] // test run1[0].forEach(function(item){ let name = item.name, key = name.slice(0,2), value = item.displayValue, data ='' let notation = rfp_red // they're all red if only a single run: we green up on second runs let hasChunk = false oRes[name] = {} oRes[name][1] = value if (undefined !== oErrors[name]) { aSkip.push(name) value = oErrors[name]; notation = rfp_red; data = zErrLog } else { if (!isGecko) { if ('ge' == key) {data = zNA} // test is random, return a stable FP } else { if ('ge' == key) { // run 1 check returns mini(dataDrawn) == mini(data) let getCheck = check_canvas_get(name, 1) if (getCheck) { data = 'trustworthy' // the test is random, return a stable FP aSkip.push(name) } else { data = 'protected' countFake++ } } else { // chunk test // gecko: we can already detect tampering since we use known hashes // but in future we might use randomness and read back the value from the png hasChunk = false // reset if ('to' == key) {aChunk.forEach(function(str){if (oData[name].includes(str)) {hasChunk = true}})} if (hasChunk) { countFake++ hasChunk = true } else { if (oKnown[name].includes(value)) { aSkip.push(name) } else { data = 'protected' countFake++ } } } } } oFP[name] = {'value': value, 'notation': notation, 'chunk': hasChunk, 'data': data} }) /* console.log(aSkip) console.log(oData) console.log(oFP) //*/ // test //aSkip = aSkip.filter(x => ![toBlob].includes(x)) // we're testing for protection so always do two passes, including gecko basic mode // ToDo: handle canvas spoofing in nonGecko: e.g. we can easily test getImageData: for now just exit if (countFake == 0 || !isGecko) { exit() return } const proxyMap = { convertToBlob: 'OffscreenCanvas', getImageData: 'CanvasRenderingContext2D', isPointInPath: 'CanvasRenderingContext2D', isPointInStroke: 'CanvasRenderingContext2D', toBlob: 'HTMLCanvasElement', toDataURL: 'HTMLCanvasElement', } // smart + some lies, do 2nd run // for non skips, force a redraw oDrawn = {'get': false, 'get_solid': false, 'path': false, 'to': false, 'to_solid': false} Promise.all([ known.createHashes(window, 2) ]).then(function(run2){ run2[0].forEach(function(item){ let name = item.name, key = name.slice(0,2), proxyname = name.replace('_solid', '') let value = item.displayValue let checkValue = value let hasChunk = false // getImageData doesn't get a 'skip' so we handle it differently // don't check if already skipped: e.g. type error null // run2 check returns skip if nothing to do, or true/false if RFP-like // why do I need this? if ('ge' == key && 'skip' !== checkValue) { let getCheck = check_canvas_get(name, 2) if ('skip' == getCheck) {checkValue = 'skip'} } if (checkValue !== 'skip') { let data ='', notation ='', stats ='', rfpvalue ='', isChunk ='' // proxy let isProxy = isProxyLie(proxyMap[proxyname] +'.'+ proxyname) // chunk test hasChunk = false // reset if ('to' == key) { aChunk.forEach(function(str){ if (oData[name].includes(str)) {hasChunk = true} }) } if (hasChunk) { // privacyX, which doesn't protect toBlob yet, is causing intermittent false positive isChunk // on it's (per execution) *toDataURL when FPP is on. This is FPP kicking in somewhere due to // timing. It's not sufficient to check the chunk is persistent (but we'll do that) // I think all we can do is exclude if proxylies if (oFP[name].chunk == true && !isProxy) {isChunk = '*'} } if (oRes[name][1] == value) { // persistent let isWhite = false if ('is' == key) { notation = (value === allZeros && !isProxy) ? rfp_green : rfp_red // all zeros } else { notation = rfp_red // all white: e.g. perps stupidly being told to flip // privacy.resistFingerprinting.randomDataOnCanvasExtract if (oKnown[key +'_white'].includes(value)) {isWhite = true} // exclude BB which must fail if not RFP if (isFPPFallback) { // FPP: 119+ and no proxy lies and no getImageData stealth // FF144 or lower: exclude solids: FPP does not tamper with those // exclude if all white | exclude if proxy lies // note: isGetStealth is getImageData let useSolid = !name.includes('_solid') if (isVer > 144 && 'to' == key && isChunk !== '') {useSolid = true} // FF145+ FPP now handles to* solids if (!isWhite && useSolid) { if (!isProxy) { if ('ge' == key && !isGetStealth || 'ge' !== key) { // no proxy lies but persistent, so must be FPP notation = fpp_green } } } } } rfpvalue = notation == rfp_green ? ' | RFP' : (notation == fpp_green ? ' | FPP' : '') if ('ge' == key) { stats = isCanvasGet rfpvalue += ' | '+ isCanvasGetChannels } notation += ' [persistent' + isChunk + (isWhite ? ' white]' : ']'+ stats) data = 'protected | persistent'+ isChunk + (isWhite ? ' white' : rfpvalue) } else { // per execution if ('is' == key) { notation = rfp_red } else if ('to' == key) { notation = check_canvas_to(oData[name]) ? rfp_green : rfp_red } else { notation = check_canvas_get(name, 2) ? rfp_green : rfp_red } rfpvalue = notation == rfp_green ? ' | RFP' : '' if ('ge' == key) { stats = isCanvasGet data += ' | '+ isCanvasGetChannels } notation += ' [per execution' + isChunk +']'+ stats data = 'protected | per execution'+ isChunk + rfpvalue } oFP[name] = {'value': value, 'notation': notation, 'data': data} } }) exit() }) }) }) const outputCanvas = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('canvas')) {return resolve()} Promise.all([ get_canvas() ]).then(function(){ return resolve() }) }) countJS(9) ================================================ FILE: js/codecs.js ================================================ 'use strict'; let mediaList = {} function set_mediaList() { let v = "video/", a = "audio/" // ToDo: add wmf: e.g. 1806552 let aList = [ //'application/fake', 'application/ogg', a+'aac', a+'flac', a+'matroska', a+'mp3', a+'mp4', a+'mp4; codecs=', a+'mp4; codecs=""', a+'mp4; codecs="flac"', a+'mp4; codecs="mp3"', a+'mp4; codecs="mp4a.40.2"', a+'mp4; codecs="mp4a.40.29"', a+'mp4; codecs="mp4a.40.42"', // FF143+ 1711882 - removed FF144+ 1989946 a+'mp4; codecs="mp4a.40.5"', a+'mp4; codecs="mp4a.67"', a+'mp4; codecs="opus"', //a+'mp4; codecs=\'\'', a+'mpeg', a+'mpeg; codecs="mp3"', a+'ogg; codecs="flac"', a+'ogg; codecs="opus"', a+'ogg; codecs="vorbis"', a+'wav',a+'wav; codecs="1"', a+'wave',a+'wave; codecs="1"', a+'webm', a+'webm; codecs="opus"', a+'webm; codecs="vorbis"', a+'x-aac', a+'x-flac', a+'x-matroska', a+'x-m4a', a+'x-pn-wav',a+'x-pn-wav; codecs="1"', a+'x-wav',a+'x-wav; codecs="1"', ] let vList = [ 'application/ogg', v+'3gpp', v+'matroska', v+'matroska; codecs="av1"', v+'matroska; codecs="avc1.58000a"', v+'matroska; codecs="avc1.6e000a"', v+'matroska; codecs="avc1.64003E"', v+'matroska; codecs="avc1.7a000a"', v+'matroska; codecs="avc1.f4000a"', v+'matroska; codecs="hvc1.1.6.L186.B0"', v+'matroska; codecs="hvc1.1.6.L93.B0"', v+'matroska; codecs="hev1.1.6.L186.B0"', v+'matroska; codecs="hev1.1.6.L93.B0"', v+'matroska; codecs="vp8"', v+'matroska; codecs="vp9"', v+'mp4', v+'mp4; codecs=', v+'mp4; codecs=""', v+'mp4; codecs="av01.0.08M.08"', // 8bit v+'mp4; codecs="av01.0.00M.10"', // 10bit v+'mp4; codecs="av01.0.00M.12"', // 12bit v+'mp4; codecs="av01.2.31H.12"', v+'mp4; codecs="avc1"', v+'mp4; codecs="avc1.58000a"', // extended v+'mp4; codecs="avc1.6e000a"', // high 10 v+'mp4; codecs="avc1.64003E"', v+'mp4; codecs="avc1.7a000a"', // high 4:2:2 v+'mp4; codecs="avc1.f4000a"', // high 4:4:4 v+'mp4; codecs="avc3"', v+'mp4; codecs="avc3.64003E"', v+'mp4; codecs="flac"', v+'mp4; codecs="hev1.1.6.L186.B0"', //v+'mp4; codecs="hev1.1.6.L186.B0, mp4a.40.2"', v+'mp4; codecs="hev1.1.6.L93.B0"', // 1853448 v+'mp4; codecs="hev1.2.4.L120.B0"', v+'mp4; codecs="hvc1.1.6.L186.B0"', v+'mp4; codecs="hvc1.1.6.L93.B0"', v+'mp4; codecs="hvc1.2.4.L120.B0"', v+'mp4; codecs="opus"', v+'mp4; codecs="vp09.00.10.08"', v+'mp4; codecs="vp9"', //v+'mp4; codecs=\'\'', v+'quicktime', v+'webm', v+'webm; codecs="av01"', v+'webm; codecs="av1"', v+'webm; codecs="vorbis"', v+'webm; codecs="vp8"', v+'webm; codecs="vp8, opus"', v+'webm; codecs="vp8, vorbis"', v+'webm; codecs="vp9"', v+'webm; codecs="vp9, opus"', v+'webm; codecs="vp9, vorbis"', v+'x-m4v', v+'x-matroska', // 1986058 FF144+ v+'x-matroska; codecs="av1"', v+'x-matroska; codecs="av1, opus"', v+'x-matroska; codecs="avc1.58000a"', v+'x-matroska; codecs="avc1.6e000a"', v+'x-matroska; codecs="avc1.64003E"', //v+'x-matroska; codecs="avc1.64003E, opus"', v+'x-matroska; codecs="avc1.7a000a"', v+'x-matroska; codecs="avc1.f4000a"', v+'x-matroska; codecs="hvc1.1.6.L186.B0"', v+'x-matroska; codecs="hvc1.1.6.L93.B0"', v+'x-matroska; codecs="hev1.1.6.L186.B0"', v+'x-matroska; codecs="hev1.1.6.L93.B0"', v+'x-matroska; codecs="vp8"', v+'x-matroska; codecs="vp9"', ] if (isVer < 130) { // theora support: FF126 1860492 prep + FF130 1890370 remove vList.push( v+'ogg', v+'ogg; codecs="flac"', v+'ogg; codecs="opus"', v+'ogg; codecs="theora"', v+'ogg; codecs="theora, flac"', v+'ogg; codecs="theora, speex"', v+'ogg; codecs="theora, vorbis"', ) } // add and record fakes // we use default 5 for length to ensure we don't randomly duplicate a real codec/mime name mediaList['fake'] = { 'audio': ['application/'+ rnd_word(), a + rnd_word(), a+'mp4; codecs="'+ rnd_word() +'"'], 'video': ['application/'+ rnd_word(), v + rnd_word(), v+'mp4; codecs="'+ rnd_word() +'"', v+'webm; codecs="'+ rnd_word() +'"'] } for (const k of Object.keys(mediaList.fake)) { let aFake = mediaList.fake[k] aFake.forEach(function(item){if ('audio' == k) {aList.push(item)} else {vList.push(item)}}) } mediaList['audio'] = aList.sort() mediaList['video'] = vList.sort() let mediaBtn = addButton(13, 'audio_codecs', aList.length +' audio', 'btnc', 'lists') + addButton(13, 'video_codecs', vList.length +' video', 'btnc', 'lists') addDisplay(13, 'mediaBtn', mediaBtn) } function get_autoplay(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getAutoplayPolicy // a check on a specific element is more reliable (though it doesn't matter on page load) // cached from page load let value, data ='', notation = isDesktop ? default_red : '' if (undefined == isAutoPlayError) { // Note: this is inconsistent/unstable on android: e.g. can return 'disallowed | disallowed' if the // phone is on 'Do Not Disturb' )or depending on the session and transient user activity/actions?) if (isDesktop && '5be5c665' == mini(isAutoPlay)) {notation = default_green} value = isAutoPlay } else { value = isAutoPlayError; data = isAutoPlay } addBoth(13, METRIC, value,'', notation, data) // user: not part of FP; don't record errors etc const METRICuser = METRIC +'_user' if (gLoad || 'undefined' == isAutoPlay) { addDisplay(13, METRICuser, zNA) return } try { let atest, mtest let ares = navigator.getAutoplayPolicy('audiocontext') try {atest = navigator.getAutoplayPolicy(dom.tzpAudio)} catch {atest = zErr} let mres = navigator.getAutoplayPolicy('mediaelement') try {mtest = navigator.getAutoplayPolicy(dom.tzpVideo)} catch {mtest = zErr} let display = (ares === atest ? ares : ares +', '+ atest) +' | '+ (mres === mtest ? mres : mres +', '+ mtest) addDisplay(13, METRICuser, display) } catch(e) { addDisplay(13, METRICuser, (e+'').slice(0,47) + '...') } return } function get_capabilities_rfc(type) { // https://developer.mozilla.org/en-US/docs/Web/API/RTCCodecStats/sdpFmtpLine // https://w3c.github.io/webrtc-stats/#dom-rtccodecstats-sdpfmtpline // https://datatracker.ietf.org/doc/html/rfc7587 const METRIC = type +'_getCapabilities_rtc' let hash, data = '', btn ='', notation = isTB ? bb_red : '' try { if (runSE) {foo++} let receiver = window.RTCRtpReceiver if (runST) {receiver = []} let typeCheck = typeFn(receiver) if ('undefined' == typeCheck) { hash = typeCheck if (isTB) {notation = bb_green} } else if ('function' !== typeCheck) { throw zErrType + typeCheck } else { data = receiver.getCapabilities(type) if (runSI) {data = null} typeCheck = typeFn(data) if ('object' !== typeCheck) {throw zErrInvalid +'expected object: got '+ typeCheck} //console.log(data) hash = mini(data); btn = addButton(13, METRIC) } } catch(e) { hash = e; data = zErrLog } addBoth(13, METRIC, hash, btn, notation, data) return } function get_codecs(type) { // https://privacycheck.sec.lrz.de/active/fp_cpt/fp_can_play_type.html // https://cconcolato.github.io/media-mime-support/ let t0 = nowFn() const metricC = type +'_canPlayType', metricT = type +'_isTypeSupported' let list = mediaList[type], countMax = list.length, aFake // canPlayType function get_canPlay() { let hash, data = {'maybe': [],'probably': []}, btn='', isLies = false, hasFake = false try { list.forEach(function(item) { let tmp = item.replace(type +'\/','') // strip 'video/','audio/' let value = obj.canPlayType(item) if (runST) {value = 'audio' == type ? undefined : ' '} let typeCheck = typeFn(value) if ('string' !== typeCheck && 'empty string' !== typeCheck) {throw zErrType + typeCheck} if ('maybe' === value || 'probably' === value) { data[value].push(tmp) if (aFake.includes(item)) {hasFake = true} } else if (runSL) { if (aFake.includes(item)) { if (item.includes('codecs')) {data.probably.push(tmp)} else {data.maybe.push(tmp)} hasFake = true } } }) // tests //data = {'maybe': [], 'probably': []} // none //data = {'maybe': [], 'probably': mediaList[type]} // all //data.maybe = [] // one empty // counts let countMaybe = data.maybe.length, countProbably = data.probably.length, countTotal = countMaybe + countProbably // lies if (0 == countTotal || countMax == countTotal) { // can't be none, all hash = (0 == countTotal ? 'none' : 'all'); data = ''; isLies = true } else { data.maybe.sort() // we removed leading audio/ and video/, so do a final sort data.probably.sort() if (hasFake) { // can't have fake isLies = true } else if (0 == countMaybe || 0 == countProbably) { // either is empty isLies = true } if (!isLies) { // probably: should only include "codecs="something"" data['probably'].forEach(function(item) { if (!item.includes("codecs=\"")) {isLies = true} }) } if (!isLies) { // maybe: shouldn't include "codecs="something"" (i.e it has "mp4; codecs=","mp4; codecs=\"\"") data['maybe'].forEach(function(item) { if (item.includes("codecs=\"")) { if (item !== "mp4; codecs=\"\"") {isLies = true} } }) } hash = mini(data), btn = addButton(13, metricC, countMaybe +'/' + countProbably) } } catch(e) { hash = e; data = zErrLog } addBoth(13, metricC, hash, btn, '', data, isLies) return } // isTypeSupported function get_isType() { let hash, data = {'MediaRecorder': [],'MediaSource': []}, btn='', isLies = false, hasFakeR = false, hasFakeS = false try { let canRecord = true, canSource = true list.forEach(function(item) { let tmp = item.replace(type +'\/','') // strip 'video/','audio/' if (canRecord) { try { let value = MediaRecorder.isTypeSupported(item) if (runST) {value = type == 'audio' ? undefined : ''} let typeCheck = typeFn(value) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} if (value) { data.MediaRecorder.push(tmp) if (aFake.includes(item)) {hasFakeR = true} } else if (runSL) { if (aFake.includes(item)) { data.MediaRecorder.push(tmp) hasFakeR = true } } //foo++ } catch(e) { hasFakeR = false canRecord = false // stop testing data.MediaRecorder = zErr // replace data log_error(13, metricT +"_MediaRecorder", e) // log error } } if (canSource) { try { let value = MediaSource.isTypeSupported(item) if (runST) {value = type == 'audio' ? 1 : null} let typeCheck = typeFn(value) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} if (value) { data.MediaSource.push(tmp) if (aFake.includes(item)) {hasFakeS = true} } else if (runSL) { if (aFake.includes(item)) { data.MediaSource.push(tmp) hasFakeS = true } } //foo++ } catch(e) { hasFakeS = false canSource = false; data.MediaSource = zErr; log_error(13, metricT +"_MediaSource", e) } } }) // both errors? if (!canRecord && !canSource) { hash = zErr; data = '' } else { // test //data = {'MediaRecorder': [],'MediaSource': []} //data.MediaRecorder = []; hasFakeR = false //data.MediaSource = []; hasFakeS = false // counts for display let countRecord = canRecord ? data.MediaRecorder.length : zErr, countSource = canSource ? data.MediaSource.length: zErr // lies if (0 == countRecord && 0 == countSource) { hash = 'none'; data = ''; isLies = true } else { if (canRecord) {data.MediaRecorder.sort()} // we removed leading audio/ and video/, so do a final sort if (canSource) {data.MediaSource.sort()} if (0 !== (hasFakeR + hasFakeS)) { // can't have fake: note each fake was set as false if we errored isLies = true } else if (canRecord && 0 == countRecord || canSource && 0 == countSource) { // either is empty and not an error isLies = true } hash = mini(data), btn = addButton(13, metricT, countRecord +'/' + countSource) } } } catch(e) { hash = e; data = zErrLog } addBoth(13, metricT, hash, btn, '', data, isLies) return } try { if (runSE) {foo++} var obj = document.createElement(type) aFake = mediaList.fake[type] Promise.all([ get_canPlay(), get_isType(), ]).then(function(){ log_perf(13, type, t0) return }) } catch(e) { addBoth(13, metricC, e, '', '', zErrLog) addBoth(13, metricT, e, '', '', zErrLog) return } } const get_eme = (METRIC) => new Promise(resolve => { /* https://w3c.github.io/encrypted-media/#common-key-systems gecko only supports 'org.w3.clearkey' 'com.widevine.alpha' other 'com.microsoft.playready', 'com.youtube.playready', 'webkit-org.w3.clearkey', 'com.adobe.primetime', 'com.adobe.access', 'com.apple.fairplay' note: media.gmp-gmpopenh264.enabled = no effect even after a restart note: 1706121 FF128+ fixed PB mode */ /* widevine gecko issues triggers DRM prompt if disabled ^ error is "NotSupportedError: EME has been preffed off" ^ this eats viewport/inner pixels on android it can hold up the result and we end up with eme == timeout ^ if rerun/no-timeout we get " error is: NotSupportedError: The application embedding this user agent has blocked MediaKeySystemAccess" on android DRM in PB mode is always prompted */ let isDone = false // really slow on first session loads in blink / also android needs help let timeout = 'blink' == isEngine ? 4000 : 400 setTimeout(function() {if (!isDone) {exit(zErrTime)}}, timeout) function exit(value, data ='', btn='') { if (!isDone) { isDone = true // results are not guaranteed to come back in the order requested: sort into a new object if ('object' == typeof data) { let newobj = {} for (const k of Object.keys(data).sort()) { newobj[k] = {} for (const j of Object.keys(data[k]).sort()) {newobj[k][j] = data[k][j]} } data = newobj value = mini(data) btn = addButton(13, METRIC) } let notation = isBB ? bb_red : '' if (isBB && '1f5a84f8' == value) {notation = bb_green} // desktop + android addBoth(13, METRIC, value, btn, notation, data) return resolve() } } let oEME = { clearkey: ['org.w3.clearkey','webkit-org.w3.clearkey'], fairplay: ['com.apple.fairplay'], playready: ['com.microsoft.playready','com.youtube.playready'], primetime: ['com.adobe.access','com.adobe.primetime'], widevine: ['com.widevine.alpha'], } // widevine on non-BB android is problematic if (!isBB && !isDesktop) {delete oEME.widevine} try { if (runSE) {foo++} let request = window.navigator.requestMediaKeySystemAccess if (runST) {request = ''} let typeCheck = typeFn(request) if ('undefined' == typeCheck) {exit(typeCheck) } else if ('function' !== typeCheck) {throw zErrType +'requestMediaKeySystemAccess: ' + typeCheck } else { let data = {}, maxCount = 0, counter = 0 for (const k of Object.keys(oEME)) {maxCount += oEME[k].length} const config = { initDataTypes: ['keyids', 'webm'], audioCapabilities: [{contentType: 'audio/webm; codecs="opus"'}], } for (const key of Object.keys(oEME).sort()) { data[key] = {} let value oEME[key].forEach(function(item){ navigator.requestMediaKeySystemAccess(item, [config]).then((result) => { typeCheck = typeFn(result) if ('empty object' !== typeCheck) {throw zErrType + typeCheck} let expected = '[object MediaKeySystemAccess]' if (result +'' !== expected) {throw zErrInvalid + 'expected '+ expected +': got '+ result} data[key][item] = true counter++ // await all results if (maxCount == counter) {exit('', data)} }).catch(function(e){ value = zErr // suppress expected errors // ToDo: check safari let aCheck = [] if (isGecko) { if (isBB) { let checkvalue = 'Key system is unsupported' if ('com.widevine.alpha' == item) {checkvalue = 'EME has been preffed off' } else if ('org.w3.clearkey' == item) {checkvalue = 'CDM is not installed'} aCheck.push('NotSupportedError: '+ checkvalue) } else { aCheck.push('NotSupportedError: Key system is unsupported') } } else if ('blink' == isEngine) { aCheck.push('NotSupportedError: Unsupported keySystem or supportedConfigurations.') } if (aCheck.includes(e+'')) { value = false } else { log_error(13, METRIC +'_'+ item, e) // item names are unique, we don't need the key } data[key][item] = value counter++ // wait for all the results if (maxCount == counter) {exit('', data)} }) }) } } } catch(e) { exit(e, zErrLog) } }) function get_preload_media(METRIC) { // FF142+/ESR140: 1972600 | also see 1969210 // ToDo: I don't think this test is sufficient, we need some actual media let value, data = '', notation = rfp_red try { value = dom.tzpAudio.preload if (runST) {value = 99} else if (runSI) {value = 'banana'} if ('string' !== typeFn(value, true)) {throw zErrType + typeFn(value)} if ('' == value) {value = typeFn(value)} let aValid = ['auto','metadata','none'] if (isVer < 140) {aValid.push('empty string')} // 929890 if (!aValid.includes(value)) {aValid.sort(); throw zErrInvalid +'expected ' + aValid.join(', ') + ': got '+ value} if ('auto' == value) {notation = rfp_green} } catch(e) { value = e; data = zErrLog } addBoth(13, METRIC, value,'', notation, data) return } const outputMedia = () => new Promise(resolve => { if (gLoad) {set_mediaList()} if (gRun && sectionIgnore.includes('codecs')) {return resolve()} if (gRun) { addDetail('audio_codecs', mediaList['audio'], 'lists') addDetail('video_codecs', mediaList['video'], 'lists') } Promise.all([ get_eme('eme'), get_codecs('audio'), get_codecs('video'), get_preload_media('preload_htmlmediaelement'), get_autoplay('getAutoplayPolicy'), get_capabilities_rfc('audio'), get_capabilities_rfc('video'), ]).then(function(){ return resolve() }) }) countJS(13) ================================================ FILE: js/css.js ================================================ 'use strict'; function rgba2hex(orig, hexOnly = false) { var a, isPercent, rgb = orig.replace(/\s/g, '').match(/^rgba?\((\d+),(\d+),(\d+),?([^,\s)]+)?/i), alpha = (rgb && rgb[4] || '').trim(), hex = rgb ? (rgb[1] | 1 << 8).toString(16).slice(1) + (rgb[2] | 1 << 8).toString(16).slice(1) + (rgb[3] | 1 << 8).toString(16).slice(1) : orig; if (alpha !== '') {a = alpha } else { a = 0o1 rgb = rgb.slice(0, rgb.length - 1) } // multiply before convert to HEX a = ((a * 255) | 1 << 8).toString(16).slice(1) hex = hex + a if (!hexOnly) { rgb = rgb.slice(1, rgb.length) hex += ' '+ rgb.join('-') } return hex } function get_colors() { let t0 = nowFn() /* https://www.w3.org/TR/css-color-4/ */ let oList = { // sorted css4: [ '-moz-activehyperlinktext','-moz-default-color','-moz-default-background-color', '-moz-hyperlinktext','-moz-visitedhyperlinktext', 'AccentColor','AccentColorText','ActiveText','ButtonBorder','ButtonFace','ButtonText', 'Canvas','CanvasText','Field','FieldText','GrayText','Highlight','HighlightText','LinkText', 'Mark','MarkText','SelectedItem','SelectedItemText','VisitedText', ], deprecated: [ 'ActiveBorder','ActiveCaption','AppWorkspace','Background','ButtonHighlight','ButtonShadow', 'CaptionText','InactiveBorder','InactiveCaption','InactiveCaptionText','InfoBackground', 'InfoText','Menu','MenuText','Scrollbar','ThreeDDarkShadow','ThreeDFace','ThreeDHighlight', 'ThreeDLightShadow','ThreeDShadow','Window','WindowFrame','WindowText', ], moz: [ '-moz-cellhighlight','-moz-cellhighlighttext','-moz-combobox','-moz-comboboxtext','-moz-dialog', '-moz-dialogtext','-moz-field','-moz-fieldtext','-moz-html-cellhighlight','-moz-html-cellhighlighttext', '-moz-menubarhovertext','-moz-menuhover','-moz-menuhovertext','-moz-oddtreerow', ], } /* note: windows 11: tested in FF146 (both protected with RFP) '-moz-menuhover' has an opacity (0.118) 'Menu' has an opacity (0.6) exposed when contrast control (forced colors) is enabled where do these come from (app theme, system, user prefs, prefers-color-scheme etc?) how are they calculated or are they hardcoded */ if (!isGecko) { delete oList.moz addBoth(14,'colors_moz', zNA) } else { // with forced colors, removed -moz named colors will be false positives (and our alpha setting is retained) // wrecking our RFP deterministic hash: to solve this we will add them if we expect them let aAdd = [] if (isVer < 141) {aAdd.push('-moz-buttonhoverface','-moz-buttonhovertext')} // removed FF141: 1968925 if (isVer < 140) {aAdd.push('-moz-eventreerow')} // removed FF140: can't find bugzilla if (aAdd.length) { oList.moz = oList.moz.concat(aAdd).sort() } } //console.log(oList) for (const type of Object.keys(oList)) { const element = dom.tzpColor const strColor = 'rgba(1, 2, 3, 0.5)' // opacity used to help avoid collisions const METRIC = 'colors_'+ type let hash, btn ='', data = {}, notation = 'moz' == type ? rfp_red : '' try { if (runSE) {foo++} let aTemp = [], oTemp = {}, aList = oList[type] aList.forEach(function(style){ element.style.backgroundColor = strColor // reset color element.style.backgroundColor = style let rgb = window.getComputedStyle(element, null).getPropertyValue('background-color') if (rgb !== strColor) { // drop obsolete aTemp.push(style +':'+ rgb) if (oTemp[rgb] == undefined) {oTemp[rgb] = [style]} else {oTemp[rgb].push(style)} } }) let tmpobj = {}, count = 0 for (const k of Object.keys(oTemp)) {tmpobj[rgba2hex(k)] = oTemp[k]} // rgba2hex for (const k of Object.keys(tmpobj).sort()) {data[k] = tmpobj[k]; count += data[k].length} // sort/count hash = mini(data); btn = addButton(14, METRIC, Object.keys(data).length +'/'+ count) if ('moz' == type) { let expectedhash = isVer == 140 ? 'c04857b2' : '2439d123' // FF140 | FF141+ notation = expectedhash == hash ? rfp_green : rfp_red } } catch(e) { hash = e; data = zErrLog } addBoth(14, METRIC, hash, btn, notation, data) } log_perf(14, 'colors', t0) return } function get_computed_styles(METRIC) { /* https://github.com/abrahamjuliot/creepjs */ let t0 = nowFn() const names = ['cssrulelist','domparser','getcomputed','htmlelement',] let aErr = [false, false, false, false] let aHashes = [], intHashes = [], oDisplay = {} let notation = isBBESR ? bb_red : '', isLies = false let styleVersion = type => { return new Promise(resolve => { // get CSSStyleDeclaration try { if (runSE) {foo++} let cssStyleDeclaration = ( type == 0 ? document.styleSheets[0].cssRules[0].style : type == 1 ? ((new DOMParser).parseFromString('', 'text/html')).body.style : type == 2 ? getComputedStyle(document.body) : type == 3 ? document.body.style : undefined ) if (!cssStyleDeclaration) { throw new TypeError('invalid argument string') } // get properties let prototype = Object.getPrototypeOf(cssStyleDeclaration), prototypeProperties = Object.getOwnPropertyNames(prototype), ownEnumerablePropertyNames = [], cssVar = /^--.*$/ // chrome getComputedStyle prepends "-" to some webkit* keys which it doesn't do in the other methods if (type == 2 && 'blink' === isEngine) {cssVar = /^-.*$/} Object.keys(cssStyleDeclaration).forEach(key => { let numericKey = !isNaN(key), value = cssStyleDeclaration[key], customPropKey = cssVar.test(key), customPropValue = cssVar.test(value) if (numericKey && !customPropValue) { return ownEnumerablePropertyNames.push(value) } else if (!numericKey && !customPropKey) { return ownEnumerablePropertyNames.push(key) } return }) // get properties in prototype chain (required only in chrome) let propertiesInPrototypeChain = {} let capitalize = str => str.charAt(0).toUpperCase() + str.slice(1), uncapitalize = str => str.charAt(0).toLowerCase() + str.slice(1), removeFirstChar = str => str.slice(1), caps = /[A-Z]/g ownEnumerablePropertyNames.forEach(key => { if (propertiesInPrototypeChain[key]) { return } // determine attribute type let isNamedAttribute = key.indexOf('-') > -1, isAliasAttribute = caps.test(key) // reduce key for computation let firstChar = key.charAt(0), isPrefixedName = isNamedAttribute && firstChar == '-', isCapitalizedAlias = isAliasAttribute && firstChar == firstChar.toUpperCase() key = ( isPrefixedName ? removeFirstChar(key) : isCapitalizedAlias ? uncapitalize(key) : key ) // find counterpart in CSSStyleDeclaration object or its prototype chain if (isNamedAttribute) { let aliasAttribute = key.split('-').map((word, index) => index == 0 ? word : capitalize(word)).join('') if (aliasAttribute in cssStyleDeclaration) { propertiesInPrototypeChain[aliasAttribute] = true } else if (capitalize(aliasAttribute) in cssStyleDeclaration) { propertiesInPrototypeChain[capitalize(aliasAttribute)] = true } } else if (isAliasAttribute) { let namedAttribute = key.replace(caps, char => '-' + char.toLowerCase()) if (namedAttribute in cssStyleDeclaration) { propertiesInPrototypeChain[namedAttribute] = true } else if (`-${namedAttribute}` in cssStyleDeclaration) { propertiesInPrototypeChain[`-${namedAttribute}`] = true } } return }) // compile keys let keys = [ ...new Set([ ...prototypeProperties, ...ownEnumerablePropertyNames, ...Object.keys(propertiesInPrototypeChain) ]) ] /* checks let moz = keys.filter(key => (/moz/i).test(key)).length, webkit = keys.filter(key => (/webkit/i).test(key)).length, prototypeName = ('' + prototype).match(/\[object (.+)\]/)[1] //*/ // output return resolve({ keys, //moz, //webkit, //prototypeName }) } catch(e) { aErr[type] = true return resolve(log_error(14, METRIC +'_'+ names[type], e)) } }) } function display() { for (const k of Object.keys(oDisplay)) {addDisplay(14, k, oDisplay[k])} log_perf(14, METRIC, t0) } // run Promise.all([ styleVersion(0), styleVersion(1), styleVersion(2), styleVersion(3), ]).then(res => { // simulate /* different hashes: !isLies res[0] = res[1]; aErr[0] = false res[2]['keys'] = ['a','constructor'] //*/ //* different hashes: isLies //res[2]['keys'] = ['a'] //*/ /* some same hashes: constructor not last res[1]['keys'].push('a') res[2]['keys'].push('a') //*/ /* various errors res[0] = {}; aErr[0] = false res[1] = {'keys': ['a','b']} res[2] = {'keys': 5} //*/ //console.log(res) for (let i=0; i < res.length; i++) { let obj = res[i] let type = METRIC +'_'+ names[i] if (aErr[i]) { oDisplay[type] = obj // error already logged } else { try { if (runST) {if (i == 1) {obj = {keys: []}} else if (i == 2) {obj = {}} else {obj = null}} let typeCheck = typeFn(obj) if ('object' !== typeCheck) {throw zErrType + typeCheck} typeCheck = typeFn(obj.keys) if ('array' !== typeCheck) {throw zErrType + 'keys: '+ typeCheck} let data = obj.keys if ('blink' == isEngine) {data.sort()} // sort for blink let hash = mini(data) aHashes.push(hash) intHashes.push(i) oDisplay[type] = hash // last item s/be constructor: detects if items are added, not removed if (data[data.length-1] !== 'constructor') {isLies = true} } catch(e) { aErr[i] = true oDisplay[type] = log_error(14, type, e) } } } let hash, btn='', data ='' if (aErr.every(x => x === true)) { // max errors hash = zErr } else { aHashes = dedupeArray(aHashes) // same hashes if (aHashes.length === 1) { hash = aHashes[0], data = res[intHashes[0]]['keys'] btn = addButton(14, METRIC, data.length) // health: BB only if ESR if (isBBESR) { if ('mac' == isOS) { /* mac has MozOsxFontSmoothing,-moz-osx-font-smoothing, WebkitFontSmoothing,-webkit-font-smoothing,webkitFontSmoothing */ if ('1c5fe54d' == hash) {notation = bb_green} // BB15 1127 } else { // https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/41347 // some older (mostly unsupported) win10 and android <= 6 will lack // fontOpticalSizing, font-optical-sizing, fontVariationSettings, font-variation-settings // but I consider these out-of-scope if ('ed89a929' == hash) {notation = bb_green} // BB15 1122 } } } else { // mixed hashes hash = 'mixed'; isLies = true // gecko is never mixed // for the first of each unique hash add sDetail + update display let aDone = {} intHashes.forEach(function(item) { let name = METRIC +'_'+ names[item], hash = oDisplay[name] if (aDone[hash] == undefined) { aDone[hash] = name sDetail[isScope][name] = res[item]['keys'] oDisplay[name] = hash + addButton(14, name, res[item]['keys'].length) } }) } } addBoth(14, METRIC, hash, btn, notation, data, isLies) display() return }).catch(e => { addBoth(14, METRIC, e,'', notation, zErrLog) return }) } function get_link(METRIC) { // FF120+ 1858397: layout.css.always_underline_links // FF143+ 1980562: returns 'none' or 'underline' let value, data ='', notation = default_red try { value = getComputedStyle(dom.tzpLink).textDecoration if (runST) {value = null} else if (runSI) {value = 'x'} let typeCheck = typeFn(value) if ('string' !== typeCheck) {throw zErrType + typeCheck} if (isGecko && isVer < 143) { if (!value.includes('rgb(')) {throw zErrInvalid +'got ' + value} } // ignore rgb values: we're using a custom value from css // but even if we weren't we already have that info from LinkText value = 'underline' == value.slice(0,9) ? 'underline' : 'none' if ('none' == value) {notation = default_green} } catch(e) { value = e; data = zErrLog } addBoth(14, METRIC, value,'', notation, data) return } function get_media_css(METRIC) { // https://drafts.csswg.org/mediaqueries-5/ let oTmpData = {}, countFail = 0, countSuccess = 0 function collect_data(metric, value, notation, data='', isLies = false) { //console.log(metric, '~'+value +'~', '~'+data+'~', notation) // data if (zErr == value) {isLies = false} oTmpData[metric] = isSmart && isLies ? zLIE : (data == '' ? value : data) // failures: we catch failures only on checked items if (rfp_red == notation) {countFail++} else if (rfp_green == notation) {countSuccess++} // display if (zLIE == oTmpData[metric]) {value = log_known(14, METRIC +'_'+ metric, value+'')} // color up + record lies addDisplay(14, METRIC +'_'+ metric, value,'', notation) } function get_mm_color(metric = 'color') { let value, isLies = false let cssvalue = getElementProp(14, '#cssC', METRIC +'_css') try { value = (function() {for (let i=0; i < 1000; i++) {if (matchMedia('(color:'+ i +')').matches === true) {return i}} return zNA // to match })() if (runSE) {foo++} else if (runSI) {value = 4.5} else if (runSL) {value = 3} if (zNA !== cssvalue) { // unfortunately, servo returns all getElementProp calls with an empty string let typeCheck = typeFn(value) if (!Number.isInteger(value)) {throw ('number' == typeCheck ? zErrInvalid +'expected Integer: got '+ value: zErrType + typeCheck)} } // lies if (cssvalue !== zErr && value !== cssvalue) {isLies = true} } catch(e) { log_error(14, METRIC +'_'+ metric, e) value = zErr } let notation = (zErr !== value && !isLies && 8 == value) ? rfp_green : rfp_red collect_data(metric, value, notation, '', isLies) collect_data(metric +'_css', '', (8 == cssvalue ? rfp_green : rfp_red), cssvalue) return } function get_mm_css() { // https://searchfox.org/mozilla-central/source/servo/components/style/gecko/media_features.rs#660 // only notate from when the mediaquery is enabled by default _and_ rfp is applied const np = 'no-preference' let oTests = { // expected 'hover': {id: 'H', test: ['hover','none']}, 'any-hover': {id: 'AH', test: ['hover','none']}, 'prefers-reduced-motion': {id: 'PRM', test: [np,'reduce'], rfp: np, rfpver: 1}, // FF63+: 1478158 'pointer': {id: 'P', test: ['fine','coarse', 'none']}, // FF64+ 'any-pointer': {id: 'AP', test: ['coarse','fine','none'], rfp: 'fine + fine', rfpver: 1}, // FF64+ // ^ any-pointer: DO NOT CHANGE ORDER: this is our after value: coarse over fine: we break on first match 'prefers-contrast': {id: 'PC', test: [np,'less','more','custom'], rfp: np, rfpver: 1}, // FF101+: 1656363 'prefers-color-scheme': {id: 'PCS', test: ['light','dark'], rfp: 'light', rfpver: 1}, // FF67+: 1494034 | and see 1643656 'forced-colors': {id: 'FC', test: ['none','active']}, // FF89+: 1659511 'dynamic-range': {id: 'DR', test: ['standard','high']}, // FF100+ 'video-dynamic-range': {id: 'VDR', test: ['standard','high'], rfp: 'standard', rfpver: 1}, // FF100+ 'update': {id: 'UD', test: ['fast','slow','none']}, // FF102+: 1422312 || FYI: gecko currently only reports none or fast // ^ https://searchfox.org/firefox-main/source/servo/components/style/gecko/media_features.rs#366 'color-gamut': {id: 'CG', test: ['rec2020','p3','srgb'], rfp: 'srgb', rfpver: 1}, // FF110+: 1422237 // ^ https://drafts.csswg.org/mediaqueries/#color-gamut: An output device can return true for multiple values of this media // feature, if it's full output gamut is large enough, or one gamut is a subset of another supported gamut // ^ we break on first match: go wide to narrow (reverse to css) // not enabled yet 'prefers-reduced-transparency': {id: 'PRT', test: [np,'reduce'], rfp: np, rfpver: 999}, // FF113+: 1736914 // layout.css.prefers-reduced-transparency.enabled: 1822176: issue to enable it 'inverted-colors': {id: 'IC', test: ['none','inverted'], rfp: 'none', rfpver: 999}, // FF114+ // 1794628: layout.css.inverted-colors.enabled 'prefers-reduced-data': {id: 'PRD', test: [np,'reduce']}, // matchmedia only: maybe collect for completeness' sake? // these are either expected values or not implemented yet /* 'environment-blending': {id: '', test: ['opaque','additive','subtractive']}, 'grid': {id: '', test: ['0','1']}, // always 0? 'nav-controls': {id: '', test: ['none','active']}, 'overflow-block': {id: '', test: ['none','scroll','paged']}, // always scroll? 'overflow-inline': {id: '', test: ['none','scroll']}, // always scroll? //'scan': {id: '', test: ['progressive','interlace']}, // noone supports this 'scripting': {id: '', test: ['enabled','initial-only','none']}, 'video-color-gamut': {id: '', test: ['srgb','p3','rec2020']}, //*/ } // ToDo: notation reduced-transparency | inverted-colors rfpver when feature enabled if (!isDesktop) { oTests['hover']['rfp'] = 'none'; oTests['hover']['rfpver'] = 1 oTests['any-hover']['rfp'] = 'none'; oTests['any-hover']['rfpver'] = 1 oTests['pointer']['rfp'] = 'coarse'; oTests['pointer']['rfpver'] = 1 oTests['any-pointer']['rfp'] = 'coarse + coarse'; } for (const metric of Object.keys(oTests)) { let isTest = '' == oTests[metric].id let id = '#css'+ oTests[metric].id let value = zNA // match css if not supported let notation ='', cssnotation ='', aTest = oTests[metric].test try { if (runSE) {foo++} for (let i=0; i < aTest.length; i++) { if (window.matchMedia('('+ metric +':'+ aTest[i] +')').matches) {value = aTest[i]; break} } if (isGecko) { // can only be a valid value or zNA if (runSL) { // run lies just pick the non true value from tests if (value == aTest[1]) {value = aTest[0]} else {value = aTest[1]} } } // same try catch so we don't concat errors if ('any-pointer' == metric) { // https://www.w3.org/TR/mediaqueries-4/#any-input // 'any-pointer, more than one of the values can match' / none = only if the others are not present let value2 = zNA aTest = ['fine','coarse','none'] // ^ any-pointer: DO NOT CHANGE ORDER: this is our before value: fine over coarse: we break on first match for (let i=0; i < aTest.length; i++) { if (window.matchMedia('('+ metric +':'+ aTest[i] +')').matches) {value2 = aTest[i]; break} } // value = after | value2 = before value = value2 + ' + '+ value } } catch(e) { if(!isTest) {log_error(14, METRIC +'_'+ metric, e)} value = zErr } if (isTest) { //console.log(metric, value) oTmpData[metric] = value } else { let cssvalue = getElementProp(14, id, metric +'_css') // don't concat errors if ('any-pointer' == metric && cssvalue !== zErr) { // this is the 1st value - we use :before let cssvalue2 = getElementProp(14, id, metric +'_css', ':before') // cssvalue = after | cssvalue2 = before let joiner = ' + ' == cssvalue.slice(0,3) ? '' : ' + ' cssvalue = cssvalue == zErr ? zErr : cssvalue2 + joiner + cssvalue } let isLies = (value !== zErr && cssvalue !== zErr && value !== cssvalue) let rfp = oTests[metric].rfp if (rfp !== undefined && isVer >= oTests[metric].rfpver) { notation = value == rfp && !isLies ? rfp_green : rfp_red cssnotation = cssvalue == rfp ? rfp_green : rfp_red } collect_data(metric, value, notation,'', isLies) collect_data(metric +'_css', '', cssnotation, cssvalue) } } } // go! get_mm_color() get_mm_css() // sort into new object let data = {} for (const k of Object.keys(oTmpData).sort()) {data[k] = oTmpData[k]} // notation let strCounts = (0 == countFail ? sg : sb) +'['+ countSuccess +'/'+ (countSuccess + countFail) +']'+ sc let medianotation = (0 == countFail ? silent_rfp_green : silent_rfp_red) // add addBoth(14, METRIC, mini(data), addButton(14, METRIC), medianotation + strCounts, data) } function get_site_colors(METRIC) { // contrast control: only used when enabled or automatic // and these override site styles // background, text, visited link, unvisited link // see: visited link colors will/may be exposed soon: https://github.com/mozilla/standards-positions/issues/1234 // all these are set by the site's css let data = {} try { // text, background-color let target = document.body //dom.tzpBody let styles = window.getComputedStyle(target) let aList = ['background-color','color'] aList.forEach(function(item){ data[item] = styles.getPropertyValue(item) }) // our 18 colors: adds entropy on whether the extension retains some semblance of color // or goes simple two-color or whatever (e.g. contrast, sepia, grayscale etc extension knobs) for (let i=1; i < 19; i++) { target = dom['tb'+i].childNodes[2].children[0].children[0] styles = window.getComputedStyle(target) let suffix = (i+'').padStart(2,'0') data['color'+ suffix] = styles.getPropertyValue('background-color') } // link, visited link aList = [ 'link', // 0,0,238 blue 'visited-link', // 85,26,139 purple ] aList.forEach(function(item){ target = dom['tzpClr'+item] styles = window.getComputedStyle(target) data[item] = styles.getPropertyValue('color') }) } catch (e) { data = zErr log_error(14, METRIC, e) } // display let hash = mini(data), btn = addButton(14, METRIC), notation = default_red let expected = ['e2399c6e','c2a28ecb'] // prefers light/dark if (expected.includes(hash)) { notation = default_green sDetail[isScope][METRIC] = data addDisplay(14, METRIC, 'original', btn, notation) addData(14, METRIC, 'original') } else { addBoth(14, METRIC, hash, btn , notation, data) } return } function get_site_styles(METRIC) { // NOTE: 'settings > contrast control' has no effect any of these /* we can't access rules SecurityError: CSSStyleSheet.rules getter: Not allowed to access cross-origin stylesheet e.g. blink 64+ changed to match spec: https://www.w3.org/TR/cssom-1/#the-cssstylesheet-interface because of this, hashing them is super stable even if some rules change values */ // proxy let metric = 'proxy', tmpArray = [], data = {} let aList = ['CSSStyleDeclaration.removeProperty','CSSStyleDeclaration.setProperty', 'Document.adoptedStyleSheets','Document.styleSheets','Element.attachShadow'] aList.forEach(function(item){ if (isProxyLie(item)) {tmpArray.push(item)} }) data['proxy'] = tmpArray.length ? tmpArray : 'none' // styleElement metric = 'styleElement', tmpArray = [] try { let target = window.document.all for (let i=2; i < 50; i++) { // start at 2 (0 and 1 are html and head) let item = target[i], type = item+'' if ('[object HTMLBodyElement]' == item) { break // stop here } else if ('[object HTMLStyleElement]' == type) { let classValue = item.classList.value if ('' !== classValue) {tmpArray.push(classValue)} } } data[metric] = tmpArray.length ? tmpArray : 'none' } catch(e) { data[metric] = zErr log_error(14, METRIC +'_'+ metric, e) } // styleSheets metric = 'styleSheets'; tmpArray = [] try { let ss = window.document.styleSheets data[metric] = {'hash' : mini(ss)} // list try { for(let i = 0; i < ss.length; i++) { let href = ss[i].ownerNode.attributes.href if (undefined == href) {href = ss[i].href +''} else {href = href.nodeValue} tmpArray.push(href) } if (!tmpArray.length) {tmpArray = 'none'} } catch(e) { tmpArray = zErr log_error(14, METRIC +'_'+ metric +'_list', e) } data[metric]['list'] = tmpArray } catch(e) { data[metric] = zErr log_error(14, METRIC +'_'+ metric, e) } // display let hash = mini(data), btn = addButton(14, METRIC), notation = default_red let expected = undefined !== isStylesheet ? '23bf083d' : '650f2257' // w w/out our extended window/screem range if (hash == expected) { notation = default_green sDetail[isScope][METRIC] = data addDisplay(14, METRIC, 'original', btn, notation) addData(14, METRIC, 'original') } else { addBoth(14, METRIC, hash, btn , notation, data) } return } const outputCSS = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('css')) {return resolve()} Promise.all([ get_colors(), get_media_css('media'), get_computed_styles('computed_styles'), get_link('underline_links'), get_site_colors('site_colors'), get_site_styles('site_styles'), ]).then(function(){ return resolve() }) }) countJS(14) ================================================ FILE: js/devices.js ================================================ 'use strict'; const get_battery = (METRIC) => new Promise(resolve => { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getBattery // blink only (and FF43-51 which is blocked) function exit(value, data = '') { addBoth(7, METRIC, value,'','', data) return resolve() } try { let value = navigator.getBattery if (runST) {value = ''} let typeCheck = typeFn(value) if ('undefined' == typeCheck) { // any engine e.g. disabled by fork or due to sandboxing etc exit(typeCheck) } else if ('blink' !== isEngine) { // non-blink throw zErrInvalid +'expected undefined: got '+ typeCheck } else { // blink if ('function' !== typeCheck) {throw zErrType +'getBattery: '+ typeCheck} navigator.getBattery().then((battery) => { let data = {}, aTimes = [] let oItems = { charging: 'boolean', chargingTime: 'Infinity', // integer seconds, 0 if full | Infinity if discharging dischargingTime: 'Infinity', // integer seconds | Infinity if charging level: 'number', // 0.0 to 1 } for (const k of Object.keys(oItems)) { let x = battery[k] // type check let typeCheck = typeFn(x), typeExpected = oItems[k] let isTime = 'Time' == k.slice(-4), isTimeCheck = ('number' == typeCheck && isTime) if (typeCheck !== typeExpected) {if (!isTimeCheck) {throw zErrType + k +': '+ typeCheck}} // validity if (isTimeCheck) { if (!Number.isInteger(x) || x < 0) {throw zErrInvalid + k + ': expected a positive integer: got '+ x} } else if ('level' == k) { if (x < 0 || x > 1) {throw zErrInvalid + k + ': expected 0 to 1: got '+ x} } if (Infinity == x) (x += '') if (isTime) {aTimes.push(x)} data[k] = x } // true, 0, Infinity, 1 == no battery or fully charged | else == a battery (or tampering) // note: *Times are not reliable: change of charging state during a chrome session, we can get 2 x Infinity // logic: battery exists if the level is less than 1 || charging is false || 2 x Infinity: this should be enough let fpvalue = 'unknown' if (!data.charging || data.level < 1 || 'InfinityInfinty' == aTimes.join('')) {fpvalue = true} addData(7, METRIC, fpvalue) // record object for clicking let btn = addButton(7, METRIC +'_reported') sDetail[isScope][METRIC +'_reported'] = data addDisplay(7, METRIC, fpvalue +' '+ btn + (true == fpvalue ? '' : ' [false or 100% charged]')) return resolve() }).catch(e => { exit(e, zErrLog) }) } } catch(e) { exit(e, zErrLog) } }) function get_device_integer(METRIC, proxyCheck) { // https://webkit.org/b/233381 : webkit is clamped to 4 or 8 // webkit now randomizes: https://bugzilla.mozilla.org/show_bug.cgi?id=1984333#c8 // dom.maxHardwareConcurrency : 1958598: FF139+ 128 let value, data ='', notation = rfp_red, expected = 24 let isHWC = 'hardwareConcurrency' == METRIC // 1984333: FF143+ (backported to beta) RFP: 8 if mac else 4 | FPP 4 or 8 if (isHWC) {expected = 2; if (isVer > 142 || isBB) {expected = 'mac' == isOS ? 8 : 4}} // RFP try { value = isHWC ? navigator[METRIC] : screen[METRIC] if (runST) {value += ''} else if (runSL) {addProxyLie(proxyCheck + METRIC)} if (!Number.isInteger(value)) {throw zErrType + typeFn(value)} } catch(e) { value = e; data = isHWC ? zErrLog : zErrShort } if (value == expected) { notation = rfp_green } else if (isHWC && isFPPFallback) { // non-BB: can fail RFP but may match FPP // 1984333: FF144+ FPP 4 or 8 if (isVer > 142) { if (4 == value || 8 == value) {notation = fpp_green} } } addBoth(7, METRIC, value,'', notation, data, isProxyLie(proxyCheck + METRIC)) return } function get_device_memory(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory // blink only let value, data ='' try { value = navigator.deviceMemory if (runST) {value += ''} else if (runSI) {value = 6} let typeCheck = typeFn(value) if ('undefined' == typeCheck) { // any engine e.g. disabled by fork or due to sandboxing etc value = typeCheck } else if ('blink' !== isEngine) { // non-blink throw zErrInvalid +'expected undefined: got '+ typeCheck } else { // blink if ('number' !== typeCheck) {throw zErrType + typeCheck} // https://www.w3.org/TR/device-memory/#sec-device-memory-js-api // "While implementations may choose different values, the recommended upper bound // is 8GiB and the recommended lower bound is 0.25GiB (or 256MiB)" // blink 147+: (approx march 2026) https://chromestatus.com/feature/6330376953921536 /* Update to a new set of possible values for the Device Memory API: - Android: 1, 2, 4, 8 - Others: 2, 4, 8, 16, 32 Replacing the old values of 0.25, 0.5, 1, 2, 4, 8 which have grown outdated. */ let aValid = [0.25, 0.5, 1, 2, 4, 8, 16, 32] if (!aValid.includes(value)) { throw zErrInvalid +'expected '+ aValid.join(', ') +': got '+ value } } } catch(e) { value = e; data = zErrLog } addBoth(7, METRIC, value,'','', data, isProxyLie('Navigator.'+ METRIC)) return } function get_device_posture(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/devicePosture // currently blink (132+) only let oData = {}, aValid = ['continuous','folder'] function get_nav(m) { let value try { value = navigator[m] if ('webkit' == isEngine && runST) {value = false} else if (runSI) {value = 'blink' == isEngine ? {} : 'folder'} let typeCheck = typeFn(value, true) if ('undefined' == typeCheck) { // any engine e.g. not implemented yet or disabled by fork or due to sandboxing etc value = typeCheck } else if ('blink' !== isEngine) { // non-blink throw zErrInvalid +'expected undefined: got '+ ('string' == typeCheck ? value : typeCheck) } else { // blink if ('object' !== typeCheck) {throw zErrType + 'devicePosture: '+ typeCheck} let expected = '[object DevicePosture]' if (value+'' !== expected) {throw zErrInvalid + 'devicePosture expected '+ expected +': got '+ value+''} value = value.type typeCheck = typeFn(value) if ('string' !== typeCheck) {throw zErrType + 'devicePosture.type: '+ typeCheck} if (!aValid.includes(value)) { throw zErrInvalid +'expected '+ aValid.join(', ') +': got '+ value } } } catch(e) { value = zErr; log_error(7, METRIC +'_'+ m, e) } oData[m] = value; addDisplay(7, METRIC +'_'+ m, value) } function get_mm(m) { let cssvalue = getElementProp(7, '#cssDP', METRIC +'_'+ m +'_css') let value = 'undefined' try { if (runSE) {foo++} for (let i=0; i < aValid.length; i++) { if (window.matchMedia('('+ m +':'+ aValid[i] +')').matches) {value = aValid[i]; break} } if ('webkit' !== isEngine && runSI) {value = 'folder'} if ('blink' !== isEngine && 'undefined' !== value) { // non-blink throw zErrInvalid +'expected undefined: got '+ value } } catch(e) { value = zErr; log_error(7, METRIC +'_'+ m, e) } oData[m] = value; addDisplay(7, METRIC +'_'+ m, value) // css if (zErr !== cssvalue) { if ('webkit' !== isEngine && runSI) {cssvalue = 'folder'} if ('blink' !== isEngine && 'undefined' !== cssvalue) { // non-blink log_error(7, METRIC +'_'+ m +'_css', zErrInvalid +'expected undefined: got '+ cssvalue) } } oData[m +'_css'] = cssvalue } // do in alphabetival order // note: since this is non-gecko we won't cross check the values match for smarts get_mm('device-posture') get_nav('devicePosture') addData(7, METRIC, oData, mini(oData)) return } const get_feature_policy = (METRIC) => new Promise(resolve => { // https://developer.mozilla.org/en-US/docs/Web/API/FeaturePolicy/allowsFeature // blink only but behind a pref for gecko 65+: dom.security.featurePolicy.webidl.enabled function exit(hash, data ='', btn ='') { addBoth(7, METRIC, hash, btn,'', data) return resolve() } try { let f = document.featurePolicy if (runST) {f = ''} else if (runSI) {f = {}} let typeCheck = typeFn(f) if ('undefined' == typeCheck) { // any engine e.g. disabled by fork or due to sandboxing etc exit(typeCheck) } else if ('webkit' == isEngine) { // webkit not supported throw zErrInvalid +'expected undefined: got '+ typeCheck } else { // blink/gecko if ('empty object' !== typeCheck) {throw zErrType + typeCheck} let expected = '[object FeaturePolicy]' if (f+'' !== expected) {throw zErrInvalid + 'expected '+ expected +': got '+ f} // enumerate: array let aList = f.features() // gecko: disabling geo or blocking geo requests or both doesn't remove geolocation // so the assumption is these have no effect and we should always have a populated array typeCheck = typeFn(aList) if ('array' !== typeCheck) {throw zErrType +'features: ' + typeCheck} // get properties: maintain order let firstItem = aList[0] let data = {'allowedFeatures': [],'false': [], 'true': []} aList.forEach(function(item){ let isFirst = item == firstItem let key = f.allowsFeature(item) if (isFirst) { //key = 'banana' typeCheck = typeFn(key) if ('boolean' !== typeCheck) {throw zErrType +' allowsFeature: '+ typeCheck} } data[key].push(item) }) // should be redundant: allowedFeatures should match data['true'] let aAllowed = [] try { aAllowed = f.allowedFeatures() //aAllowed = '' typeCheck = typeFn(aAllowed) if ('array' !== typeCheck) {throw zErrType + typeCheck} // only add if this differs let trueHash = mini(data['true']) if (trueHash == mini(aAllowed)) {delete data.allowedFeatures} else {data.allowedFeatures = aAllowed} } catch(e) { data.allowedFeatures = zErr log_error(7, METRIC +'_allowedFeatures', e) } let hash = mini(data), btn = addButton(7, METRIC) exit(hash, data, btn) } } catch(e) { exit(e, zErrLog) } }) const get_keyboard = (METRIC) => new Promise(resolve => { // https://developer.mozilla.org/en-US/docs/Web/API/Keyboard_API // blink only // https://wicg.github.io/keyboard-map/ // https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system function exit(hash, data='', btn ='') { addBoth(7, METRIC, hash, btn,'', data) return resolve() } try { let k = navigator.keyboard if (runSI) {k = []} let typeCheck = typeFn(k) if ('undefined' == typeCheck) { // any engine e.g. disabled by fork or due to sandboxing etc exit(typeCheck) } else if ('blink' !== isEngine) { // non-blink throw zErrInvalid +'expected undefined: got '+ typeCheck } else if ('object' !== typeFn(k, true)) { throw zErrType + typeCheck } else { // blink let expected = '[object Keyboard]' if (k+'' !== expected) { throw zErrInvalid + 'expected '+ expected +': got '+ (typeCheck.includes('object') ? k : typeCheck) } let aKeys = [ 'Backquote','Backslash','Backspace','BracketLeft','BracketRight','Comma','Digit0', 'Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9', 'Equal','IntlBackslash','IntlRo','IntlYen','KeyA','KeyB','KeyC','KeyD','KeyE','KeyF', 'KeyG','KeyH','KeyI','KeyJ','KeyK','KeyL','KeyM','KeyN','KeyO','KeyP','KeyQ','KeyR', 'KeyS','KeyT','KeyU','KeyV','KeyW','KeyX','KeyY','KeyZ','Minus','Period','Quote', 'Semicolon','Slash' ] k.getLayoutMap().then(keyboardLayoutMap => { // check size if (keyboardLayoutMap.size > 0) { let data = {} aKeys.forEach(function(key) {data[key] = keyboardLayoutMap.get(key)}) exit(mini(data), data, addButton(7, METRIC)) } else { // e.g. vivalid : is it this? https://wicg.github.io/keyboard-map/#permissions-policy exit('keyboardLayoutMap.size: 0') } }).catch(function(err){ exit(err, zErrLog) }) } } catch(e) { exit(e, zErrLog) } }) function get_media_constraints(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getSupportedConstraints // I doubt this adds any entropy, it's equivalency of engine/version changes // but collect it anyway as yet one more piece of the browser object model let hash, data ='', btn='' try { let m = navigator.mediaDevices if (undefined !== m) { data = [] const s = m.getSupportedConstraints() for (const c of Object.keys(s)) {data.push(c)} if (!data.length) {throw zErrInvalid +'none'} hash = mini(data); btn = addButton(7, METRIC) } } catch(e) { hash = e; data = zErrLog } addBoth(7, METRIC, hash, btn,'', data) } const get_media_devices = (METRIC) => new Promise(resolve => { let t0 = nowFn() function set_notation(value ='') { // 1528042: FF115+ media.devices.enumerate.legacy.enabled let notation ='' if (isTB) { notation = 'undefined' == value ? bb_green : bb_red } else { notation = '75e77887' == value ? rfp_green : rfp_red } return notation } function analyse(devices) { let hash ='none', btn ='', data ='' try { if (runST) {devices = undefined} else if (runSI) {devices = [{}]} let typeCheck = typeFn(devices, true) if ('array' !== typeCheck) {throw zErrType + typeFn(devices)} if (devices.length > 0) { // tampered let aSplit = (devices +'').split(',') let aValid = ['[object InputDeviceInfo]','[object MediaDeviceInfo]'] for (let i=0; i < aSplit.length; i++) { if (!aValid.includes(aSplit[i])) {throw zErrInvalid +'expected '+ aValid.join(', ') +': got '+ aSplit[i]} } // enumerate // don't combine kind, keep order, record length not strings // checking length of undefined (fake) will catch an error data = {} let sLen = new Set(), index = 0 devices.forEach(function(d) { let kind = d.kind, kindtest = kind.length, dLen = d.deviceId.length, gLen = d.groupId.length, indexKey = (index+'').padStart(2,'0') data[indexKey +'-'+ kind] = [dLen, gLen, d.label.length] sLen.add(dLen) sLen.add(gLen) index ++ // we could check valid lengths (0 or 44 in 115+: labels always 0) // and if 44 is valid then the last char is '=', and we could type check }) hash = mini(data); btn = addButton(7, METRIC, data.length) } } catch(e) { hash = e; data = zErrLog } addBoth(7, METRIC, hash, btn, set_notation(hash), data, isProxyLie('MediaDevices.enumerateDevices')) log_perf(7, METRIC, t0) return resolve() } if (undefined == navigator.mediaDevices) { addBoth(7, METRIC, 'undefined','', set_notation('undefined')) return resolve() } if (gLoad && isDevices !== undefined) { analyse(isDevices) // warmup success } else { try { let timer = 2000 if (runSG) {timer = 0} // timed out // await devices promiseRaceFulfilled({ promise: navigator.mediaDevices.enumerateDevices(), responseType: Array, limit: timer // increase race limit for slow system/networks }).then(function(devices) { if (!devices) { addBoth(7, METRIC, zErrTime,'', set_notation(zErrTime), zErrLog) // promise failed return resolve() } else { analyse(devices) } }) } catch(e) { addBoth(7, METRIC, e,'', set_notation(e+''), zErrLog) return resolve() } } }) function get_memory(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory // blink only and deprecated // super unstable in this form: just display for now function exit(hash, data ='', btn ='') { sDetail[isScope][METRIC] = data addDisplay(7, METRIC, hash, btn) //addBoth(7, METRIC, hash, btn,'', data) return } try { let k = performance.memory if (runSI) {k = [1]} let typeCheck = typeFn(k) if ('undefined' == typeCheck) { // any engine e.g. removed by blink (deprecated) or disabled by fork or due to sandboxing etc exit(typeCheck) } else if ('blink' !== isEngine) { // non-blink throw zErrInvalid +'expected undefined: got '+ typeCheck } else { // blink let expected = '[object MemoryInfo]', data = {} if (k+'' !== expected) { throw zErrInvalid + 'expected '+ expected +': got '+ (typeCheck.includes('object') ? k : typeCheck) } let aKeys = ['jsHeapSizeLimit','totalJSHeapSize','usedJSHeapSize'] aKeys.forEach(function(m){ let value, check try { value = k[m] if (runSI) {value = null} let check = typeFn(value) if ('number' !== check) {throw zErrType + check} } catch(e) { value = zErr; log_error(7, METRIC +'_'+ m, e) } data[m] = value }) exit(mini(data), data, addButton(7, METRIC)) } } catch(e) { exit(e, zErrLog) } } const get_permissions = (METRIC) => new Promise(resolve => { // https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API#permission-aware_apis // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy#directives let tmpData = {}, data = {}, count = 0 let aList = [ // gecko 'camera','geolocation','microphone','midi','midi_sysex','notifications', 'persistent-storage','push','screen-wake-lock', // non-gecko 'accelerometer','ambient-light-sensor','background-fetch','background-sync','clipboard-read', 'clipboard-write','compute-pressure','gyroscope','local-fonts','magnetometer','payment-handler', 'storage-access','top-level-storage-access','window-management', // not listed on mdn: but confirmed in blink as a non error 'display-capture','nfc', // other //'accessibility-events', // 'bluetooth','device-info','gamepad','speaker','speaker-selection', ] aList.sort() for (let i=0; i < aList.length; i++) { let k = aList[i], key = k // https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus // spec: https://w3c.github.io/permissions/#permissions let aValid = ['denied','granted','prompt'] function accrue(k, value) { count++ if (undefined ==tmpData[value]) {tmpData[value] = [k]} else {tmpData[value].push(k)} if (count == (aList.length)) {exit()} } try { if (runSE) {foo++} let isSysex = k.includes('sysex') if (isSysex) {key = 'midi'} navigator.permissions.query({name: key, sysex: isSysex}).then(function(r) { let state = r.state if (runST) {state = undefined} else if (runSI) {state = 'allowed'} // checks let typeCheck = typeFn(state) if ('string' !== typeCheck) {throw zErrType + typeCheck} if (!aValid.includes(state)) {throw zErrInvalid +'expected '+ aValid.join(', ') +': got '+ state} accrue(k, state) }).catch(err => { // only log non-standard gecko if (isGecko) { let expected = 'TypeError: \''+ k +'\' (value of \'name\' member of ' + 'PermissionDescriptor) is not a valid value for enumeration PermissionName.' if (err+'' !== expected) {log_error(7, METRIC +'_'+ k, err)} } accrue(k, zErr) }) } catch(e) { if (isGecko) {log_error(7, METRIC +'_'+ k, e)} accrue(k, zErr) } } function exit() { // sort object: sort arrays so permission delays don't create disorder for (const k of Object.keys(tmpData).sort()) {data[k] = tmpData[k].sort()} let hash = mini(data) let notation = '88b7fbf8' == hash ? default_green : default_red // record addBoth(7, METRIC, hash, addButton(7, METRIC), notation, data) return resolve() } }) function get_screen_isextended(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/Screen/isExtended // currently blink (100+) only let value, data ='' try { value = screen.isExtended if (runST) {value = 'true'} let typeCheck = typeFn(value) if ('undefined' == typeCheck) { // any engine e.g. disabled by fork or due to sandboxing etc value = typeCheck } else if ('blink' !== isEngine) { // non-blink throw zErrInvalid +'expected undefined: got '+ typeCheck } else { // blink if ('boolean' !== typeCheck && 'undefined' !== typeCheck) {throw zErrType + typeCheck} } } catch(e) { value = e; data = zErrLog } addBoth(7, METRIC, value,'','', data) return } function get_touc_h(METRIC) { // note: function name avoids "ouch" to avoid being picked up in window properties // the element keys and window properties are redundant but required for health/benign value checks // dom.w3c_touch_events.enabled: 0=disabled (macOS) 1=enabled 2=autodetect (linux/win/android) function get_maxTouchPoints(m) { // https://www.w3.org/TR/pointerevents/#extensions-to-the-navigator-interface // FF64+: RFP 1363508 let value try { value = navigator[m] if (runST) {value = undefined} else if (runSI) {value = -5} else if (runSL) {addProxyLie('Navigator.'+ m)} let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} if (!Number.isInteger(value) || value < 0) {throw zErrInvalid + 'expected +Integer: got '+ value} if (isProxyLie('Navigator.'+ m)) { log_known(7, METRIC +'_'+ m, value) value = zLIE } } catch(e) { log_error(7, METRIC +'_'+ m, e) value = zErr } data[m] = value } function get_elements_touch() { // gecko: ontouch* only exists in android: desktop blocks these to avoid being identified as mobile // and onkly android has createTouch and createTouchList in Document // ~0.06ms let eList = ['Document','HTMLElement','MathMLElement','SVGElement'] eList.forEach(function(m){ let value try { if (runSE) {foo++} let target = window[m] let typeCheck = typeFn(target) if (runST) {typeCheck = undefined} if ('function' !== typeCheck) {throw zErrType + typeCheck} let props = Object.getOwnPropertyNames(target.prototype) value = props.filter(x => x.includes('ouch')) value.sort() // we already capture order in window function properties if (0 == value.length) {value = 'none'} if (isGecko) { // gecko: ontouch* only exists in android: desktop blocks these to avoid being identified as mobile let got = 'none' == value ? value : value.join(', ') if (!isDesktop) { // android let expected = ['ontouchcancel','ontouchend','ontouchmove','ontouchstart'] if ('Document' == m) {expected.push('createTouch','createTouchList'); expected.sort()} let minihash = mini(value), miniexpected = mini(expected) if (minihash !== miniexpected) { throw zErrInvalid +'expected '+ expected.join(', ') +': got '+ got } } else if ('none' !== value) { // desktop throw zErrInvalid +'expected none: got '+ got } } } catch(e) { log_error(7, METRIC +'_'+ m, e) value = zErr } data[m] = value }) } function get_element_touch(m) { // domparser: 0.12ms | dom: 0.08 | just use domparser let value = [] try { let parser = new DOMParser let doc = parser.parseFromString('', "text/html") let target = doc.body.firstChild //let target = dom.tzpDiv // dom test for (const key in target) {if (key.includes('ouch')) {value.push(key)}} value.sort() // we already capture order in window properties if (0 == value.length) {value = 'none'} if (isGecko) { let got = 'none' == value ? value : value.join(', ') if (!isDesktop) { // android if ('30ea93d7' !== mini(value)) { let expected = ['ontouchcancel','ontouchend','ontouchmove','ontouchstart'] // ordered throw zErrInvalid +'expected '+ expected.join(', ') +': got '+ got } } else if ('none' !== value) { // desktop throw zErrInvalid +'expected none: got '+ got } } } catch(e) { log_error(7, METRIC +'_'+ m, e) value = zErr } data[m] = value } function get_window_touch(m) { // 0.4ms window | 1.2ms iframe let value try { let props = Object.getOwnPropertyNames(window) value = props.filter(x => x.includes('ouch')) value.sort() // we already capture order in window properties if (0 == value.length) {value = 'none'} if (isGecko) { let expected, got = 'none' == value ? value : value.join(', ') if ('mac' == isOS) { // mac doesn't have touch if ('none' !== value) {throw zErrInvalid +'expected none: got '+ got} } else if (!isDesktop) { // android if ('62482a70' !== mini(value)) { expected = ['Touch','TouchEvent','TouchList','ontouchcancel','ontouchend','ontouchmove','ontouchstart'] // ordered throw zErrInvalid +'expected '+ expected.join(', ') +': got '+ got } } else { // windows/linux: none or ['Touch','TouchEvent','TouchList'] if ('none' !== value && 'a8d0e340' !== mini(value)) { expected = ['Touch','TouchEvent','TouchList'] throw zErrInvalid +'expected none or '+ expected.join(', ') +': got '+ got } } } } catch(e) { log_error(7, METRIC +'_'+ m, e) value = zErr } data[m] = value } let data = {'Document': '', 'HTMLElement': '','MathMLElement': '','SVGElement': '','maxTouchPoints': '','window': ''} // pre-ordered let notation = '' //get_element_touch('element') // skip this for now since we have HTMLElement (+ Mathml + SVG) get_elements_touch() get_maxTouchPoints('maxTouchPoints') get_window_touch('window') let hash = mini(data), btn = addButton(7, METRIC) let hash0 ='727b0fac', hash1 ='0eb47178', hash5 ='f492a7f4', hash10 ='5091c020', hashA5 = 'c51b1822' /* // desktop: note: if maxTouchPoints == 0 windows = 'none' { "Document": 'none', "HTMLElement": 'none', "MathMLElement": 'none', "SVGElement": 'none', "maxTouchPoints": 5, "window": ['Touch','TouchEvent','TouchList'] } //android + 5 { "Document": ['createTouch','createTouchList','ontouchcancel','ontouchend','ontouchmove','ontouchstart'], "HTMLElement": ['ontouchcancel','ontouchend','ontouchmove','ontouchstart'], "MathMLElement": ['ontouchcancel','ontouchend','ontouchmove','ontouchstart'], "SVGElement": ['ontouchcancel','ontouchend','ontouchmove','ontouchstart'], "maxTouchPoints": 5, "window": ['Touch','TouchEvent','TouchList','ontouchcancel','ontouchend','ontouchmove','ontouchstart'] } */ // RFP // 1957658: FF143+, ESR140.2: 5 android, 10 windows, 0 mac and linux // 1991701: FF146+ (and BB15): Re-enable touch on Linux (and remove RFPTarget::PointerId) // 2021715: FF150+ 5 on linux let rfpHashes = { 'android': [hashA5], 'linux': [hash5], // FF150+ 'mac': [hash0], 'windows': [hash10], } if (isVer < 150) {rfpHashes.linux = '553ce3d9'} // maxTouchPoints = 0 notation = (undefined !== isOS && rfpHashes[isOS].includes(hash) ? rfp_green : rfp_red) // non-BB: fails RFP but may match FPP if (isFPPFallback && undefined !== isOS && notation == rfp_red) { // FPP: 1977836 FF142: 0 or 1, everything else as 5 | 1978414: ship touch points let fppHashes = { 'android': [hashA5], 'mac': [hash0], 'linux': [hash0], 'windows': [hash0, hash1, hash5], } if (isVer > 149) {fppHashes.linux.push(hash5)} // FF150+: 2020170: linux wayland can now report 5 if (fppHashes[isOS].includes(hash)) {notation = fpp_green} } addBoth(7, METRIC, hash, btn, notation, data) return } function get_viewport_segments(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries // CSS level 5 let data = {}, display = {}, aList = ['horizontal','vertical'] aList.forEach(function(m) { let value = zNA try { for (let i = 1; i < 6; i++) { // css only goes to 5 if (window.matchMedia('('+ m +'-viewport-segments:'+ i +')').matches) {value = i; break} } if (runSE) {foo++} else if (runSL) {value = 6} } catch(e) { value = zErr; log_error(7, METRIC +'_'+ m, e) } let pseudo = 'horizontal' == m ? ':before' : ':after' let cssvalue = getElementProp(7, '#cssVS', METRIC +'_'+ m +'_css', pseudo) display[m] = value if (isSmart) { if (cssvalue !== zErr && value !== zErr) { if (value !== cssvalue) { display[m] = log_known(7, METRIC +'_'+ m, value) // record and color up lies value = zLIE } } } data[m] = value data[m +'_css'] = cssvalue }) addDisplay(7, METRIC, display.horizontal +' x '+ display.vertical) addData(7, METRIC, data, mini(data)) return } const outputDevices = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('devices')) {return resolve()} addBoth(7, 'recursion', isRecursion[0],'','', isRecursion[1]) Promise.all([ get_media_devices('mediaDevices'), get_media_constraints('mediaDevices_constraints'), get_touc_h('touch'), get_device_integer('pixelDepth','Screen.'), get_device_integer('colorDepth','Screen.'), get_device_integer('hardwareConcurrency','Navigator.'), get_permissions('permissions'), get_feature_policy('featurePolicy'), // blink only | gecko behind a pref since FF65 get_viewport_segments('viewport-segments'), // blink only get_battery('battery'), get_device_memory('deviceMemory'), get_device_posture('devicePosture'), get_keyboard('keyboard'), get_memory('memory'), get_screen_isextended('screen_isextended'), ]).then(function(){ return resolve() }) }) countJS(7) ================================================ FILE: js/elements.js ================================================ 'use strict'; // element results always in this order: width, height, x, y /* it is up to the fingerprinter to ensure custom/website css doesn't influence measurements. TZP uses careful site css rules and revert as a PoC - more than enough to ensure defaults, but trying to mitigate all possible css rules is prohibitive. Perhaps one method could be to create and use an iframe on demand */ function get_domrect(METRIC) { // quick exits let hash, data = {} if (!isGecko) {hash = zNA} else if ('9e6f19c5' == mini(oDomRect)) {hash = 'trustworthy'} if (undefined !== hash) { addBoth(15, METRIC, hash) return } let control = { bottom: 120.69999694824219, height: 141.41665649414062, left: -20.716659545898438, right: 120.69999694824219, top: -20.716659545898438, width: 141.41665649414062, x: -20.716659545898438, y: -20.716659545898438 } // for each method per key in oDomRect we return either // error, trustworthy, or some FPing on the diffs // note: errors are already recorded sDetail[isScope][METRIC +'_data'] = {} let tmpdata = {} let countPass = 0 for (const k of Object.keys(oDomRect).sort()) { sDetail[isScope][METRIC +'_data'][k] = oDomRect[k] let value ='' if (zErr == k) {value = zErr } else if ('642e7ef0' == k) {value = 'trustworthy'; countPass = oDomRect[k]['methods'].length } else { value = zLIE // analyse noise let oDiffs = {}, aProps = [], max = 0 let isNegative = false, isPositive = false let test = oDomRect[k]['data'] for (const p of Object.keys(test)) { let diff = control[p] - test[p] if (diff > 0) {isPositive = true} else {isNegative = true} if (Math.abs(diff) > max) {max = Math.abs(diff)} if (0 !== diff) { aProps.push(p) if (undefined == oDiffs[diff]) {oDiffs[diff] = [p]} else {oDiffs[diff].push(p)} } } let multiples = [] for (const m of Object.keys(oDiffs)) { if (oDiffs[m].length > 1) {multiples.push(oDiffs[m].join(' + '))} } //console.log(k, oDiffs, multiples, max) // sign: chamelon seems to always be -, CB seems to always be ± let sign ='' if (isNegative && isPositive) {sign = '±'} else { sign = isNegative ? '-' : '+' } if (max > 0.1) {max = '> '+ sign +'0.1' } else { // note max is always positive var z = -Math.floor(Math.log10(max) + 1) // leading zeros // cap at 5: chameleon varies from 6 to 9 in a few tests z = z > 5 ? 5 : z max = '< '+ sign +'0.' + '0'.repeat(z-1) + '1' } value = { 'properties': aProps.length == 8 ? 'all' : aProps.join(', '), 'range': max, 'same': (multiples.length ? multiples : 'none'), 'total': Object.keys(oDiffs).length } } oDomRect[k].methods.forEach(function(method){tmpdata[method] = value}) } let btnData = addButton(15, METRIC +'_data', 'data') for (const k of Object.keys(tmpdata).sort()) {data[k] = tmpdata[k]} hash = mini(data) let btn = addButton(15, METRIC, countPass +'/4') addBoth(15, METRIC, hash, btn + btnData, default_red, data) return } function get_element_keys(METRIC) { const id = 'element-key' let hash, btn ='', data = [], notation = isBBESR ? bb_red : '', isLies = false try { if (runSE) {foo++} const element = document.createElement('a') element.setAttribute('id', id) document.body.appendChild(element) let htmlElement = dom[id] for (const key in htmlElement) {data.push(key)} hash = mini(data); btn = addButton(15, METRIC, data.length) // cydec: changes order, removes some keys // ToDo: use post constructor when we enumerate all elements const aExpected = ['scrollWidth','scrollHeight','clientWidth','clientHeight'] if ((data.reduce((a, c) => a + aExpected.includes(c), 0)) < aExpected.length) {isLies = true} // health: BB only if ESR if (isBBESR) { // 40f682b2: 352 standard // 5e5ae1c9: 365 safer (including webgl click-to-play) // the 13 items diff are all NS tampering if ('40f682b2' == hash || '5e5ae1c9' == hash) {notation = bb_green} } } catch(e) { hash = e; data = zErrLog } removeElementFn(id) addBoth(15, METRIC, hash, btn, notation, data, isLies) return } function get_element_font(METRIC, isLies) { let t0 = nowFn() // we only need a few pt values: more than enough to correlate styles // but go more in-depth with mono/serif/sans // keep in increasing size order let sizeA = ['3.9pt','141.7pt','266.6pt',] let sizeB = ['3.9pt','xx-small','x-small','small','medium','large','x-large','xx-large','xxx-large','141.7pt','266.6pt'] let oList = { // keep in sorted order // https://developer.mozilla.org/en-US/docs/Web/CSS/generic-family 'cursive': sizeA, 'emoji': sizeA, // windows: emoji = serif 'fangsong': sizeA, 'fantasy': sizeA, // windows: fantasy = sans 'math': sizeA, 'monospace': sizeB, 'sans-serif': sizeB, 'serif': sizeB, 'system-ui': sizeA, } //ToDo: each is only 3 extra tests: seem redundant // windows: they all = serif /* 'ui-monospace': sizeA, 'ui-rounded': sizeA, 'ui-serif': sizeA, 'ui-sans-serif': sizeA, //*/ const id = 'element-fp' let hash, btn ='', data = {}, method try { const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) let oData = {}, tmpobj = {} for (const k of Object.keys(oList)) { let sizes = oList[k] let tmpsizes = [], isFirst = 'cursive' == k // this is a bit iffy if we change our keys: do BETTER!! sizes.forEach(function(size) { let isTypeCheck = isFirst && size == sizes[0] // create + measure each individually as preceeding elements can affect subsequent ones dom[id].innerHTML = "
...
" let target = div.firstChild method = measureFn(target, METRIC) // width+height = max entropy AFAICT but lets add x and y becuz we can if (isTypeCheck) { if (undefined !== method.error) {throw method.errorstring} [method.width, method.height, method.x, method.y].forEach(function(item) { if (runST) {item = isLine ? undefined : '1'} let typeCheck = typeFn(item) if ('number' !== typeCheck) {throw zErrType + typeCheck} }) } tmpsizes.push([size, method.width, method.height, method.x, method.y]) }) let sizehash = mini(tmpsizes) if (oData[sizehash] == undefined) {oData[sizehash] = {data: tmpsizes, group: [k]} } else {oData[sizehash].group.push(k)} } // group by styles for (const k of Object.keys(oData)){data[oData[k].group.join(' ')] = oData[k].data} let count = Object.keys(data).length hash = mini(data); btn = addButton(15, METRIC, count +' group'+ (count > 1 ? 's' : '')) } catch(e) { hash = e; data = zErrLog } removeElementFn(id) addBoth(15, METRIC, hash, btn,'', data, isLies) log_perf(15, METRIC, t0) return } function get_element_forms(METRIC, isLies) { let t0 = nowFn() let hash, btn ='', data = {}, tmpdata = {}, newobj = {} let oList = { // ignore: hidden // redundant: (drop 2) directory, file, files // redundant: (drop 9) datetime, email, month, number, password, search, tel, text, url, week // BUT (bring back 2) month + week differ from number-etc in blink | gecko may follow suit so keep those 'native': { button: '', checkbox: '', color: '', date: '', 'datetime-local': '', details: '
', 'details_open': '
.
', file: '', image: '', month: '', number: '', progress: '', radio: '', range: '', reset: '', select: '', select_empty: '', select_empty_option: '', select_spaces: '', select_spaces_option: '', select_string: '', select_string_option: '', submit: '', textarea: '', textarea_3x5: '', time: '', week: '', }, 'unstyled': { // differ on windows // ToDo: check linux/mac/android checkbox: '', progress: '', radio: '', select: '', } } let width, height, x, y, method const id = 'element-fp' try { const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) let parent = dom[id], isFirst = true for (const key of Object.keys(oList)) { tmpdata[key] = {}, newobj[key] = {}, data[key] = {} for (const k of Object.keys(oList[key])) { // important to clear the div so no other elements can affect measurements parent.innerHTML ='' parent.innerHTML = ('' == oList[key][k] ? '' : oList[key][k]) let target = parent.firstChild // vertical seems to create subpixels in width before transform target.setAttribute('style', 'display:inline; writing-mode: vertical-lr;') if ('unstyled' == key) {target.classList.add('unstyled')} if (k.includes('_option')) {target = target.lastElementChild} method = measureFn(target, METRIC) // typecheck let itemdata = [method.width, method.height, method.x, method.y] if (isFirst) { isFirst = false if (undefined !== method.error) {throw method.errorstring} itemdata.forEach(function(item){ if (runST) {item = null} let typeCheck = typeFn(item) if ('number' !== typeCheck) {throw zErrType + typeCheck} }) } let itemhash = mini(itemdata) if (undefined == tmpdata[key][itemhash]) {tmpdata[key][itemhash] = {'data': itemdata, 'group': [k]} } else {tmpdata[key][itemhash]['group'].push(k)} } } // group by results for (const key of Object.keys(tmpdata)) { for (const k of Object.keys(tmpdata[key])) {newobj[key][tmpdata[key][k].group.join(' ')] = tmpdata[key][k]['data']} } for (const key of Object.keys(newobj)) { for (const k of Object.keys(newobj[key]).sort()) {data[key][k] = newobj[key][k]} } hash = mini(data), btn = addButton(15, METRIC) } catch(e) { hash = e; data = zErrLog } removeElementFn(id) addBoth(15, METRIC, hash, btn,'', data, isLies) log_perf(15, METRIC, t0) return } function get_element_mathml(METRIC, isLies) { let t0 = nowFn() const id = 'element-fp' const sizetype = 'px', sizes = [33,99,111], sizectl = sizes[0] let hash, btn ='', data = {}, notation = isBB ? bb_slider_red : '' try { // create element const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) let divcontrol = "
x=−b ±b2−4 ac 2a
" let mathmlstr = "x=" +"b±b24" +"ac" +"2a" let divcontent ='' sizes.forEach(function(size) { divcontent += "
"+ mathmlstr +"
" }) doc.getElementById(id).innerHTML = divcontrol + divcontent // measure let control, width, height, methodW, methodH let targetC = dom['mathmldivctrl'], targetH, targetW let isDiff, wType, hType sizes.forEach(function(size) { targetH = dom['mathmldiv'+size]; targetW = dom['mathmlspan'+size] let isCtrlSize = size == sizectl size = size + sizetype // get div height and span width methodH = measureFn(targetH, METRIC) methodW = measureFn(targetW, METRIC) width = methodW.width height = methodH.height // one time: first elment + size // get a control size (for diffs) to detemine if mathml is enabled if (isCtrlSize) { methodH = measureFn(targetC, METRIC) control = methodH.height if (undefined !== methodH.error) {throw methodH.errorstring} if (undefined !== methodH.error) {throw methodH.errorstring} if (undefined !== methodW.error) {throw methodW.errorstring} // first item check/diff if (runST) {width = {}, height = ' '} wType = typeFn(width); hType = typeFn(height) if ('number' !== wType || 'number' !== hType) { throw zErrType + (wType == hType ? wType : wType +' x '+ hType) } isDiff = height - control } data[size] = [width, height] }) let displayEnabled ='' let isEnabled = Math.abs(isDiff) > 0.001 if (!isSmart || !isLies) { data['enabled'] = isEnabled displayEnabled = ' ['+ (isEnabled ? zE : zD) +']' } if (isBB) {notation = isEnabled ? bb_standard : bb_safer} hash = mini(data); btn = addButton(15, METRIC) + displayEnabled } catch(e) { hash = e; data = zErrLog } removeElementFn(id) addBoth(15, METRIC, hash, btn, notation, data, isLies) log_perf(15, METRIC, t0) return } function get_element_other(METRIC, isLies) { let t0 = nowFn() let hash, btn ='', data = {} // note: some elements we insert a char "." to a) force a height // or b) for unique measurements without a char to get more precision/decimal places // always use the same char let aUseFirstChild = ['hgroup'] // sometimes we want the first child let oExtraStyles = { 'marquee': '; width: 20px; height: 20px', // if we don't constrain it, it changes with inner window sizes } let oList = { 'horizontal-tb' : { base: '', figure: '
', }, 'vertical-lr' : { a: '
.', audio: '', base: '', big_x2: '.', big_x3: '.', br: '
', canvas: '', caption: '
.
', dd: '
.
', dialog: '', dt: '
.
', fieldset: '
', figcaption: '
.
', geolocation: '', hgroup: '

.

.

', // code doesn't revert 2nd child so hardcode it hr: '
', img: '', legend: '
.
', li: '', 'q_empty': '', 'menu_li': '.
  • ', noembed: '.', 'ol_li': '
    1. .
    ', optgroup: '', output: '
    =
    ', plaintext: '', rt: '<ruby><rt>.</rt></ruby>', td: '<table><tr><td></td></tr></table>', tfoot: '<table><tfoot></tfoot></table>', // test error //'error': '<frame></frame>' } } let aVerticalAdd = [ 'b','big','blockquote','code','dl','h1','h2','h3','h4','h5','h6','i','iframe', 'marquee','meter','noscript','option','pre','q','small','sub','sup','ul', ] aVerticalAdd.forEach(function(item){oList['vertical-lr'][item] = '<'+item+'>.</'+item+'>'}) let width, height, x, y, method, tmpdata = {} const id = 'element-fp' try { const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) let parent = dom[id], isFirst = true for (const s of Object.keys(oList).sort()) { let style = s.slice(0,-3), itemdata tmpdata[style] = {} for (const k of Object.keys(oList[s]).sort()) { // set parent, determine target to measure and as we walk // the children, ensure no other css affects any element parent.innerHTML = oList[s][k] try { let target = parent.firstChild // revert everything for (let i = 0; i < 10; i++) { target.classList.add('revert') let newtarget = target.children[0] if (undefined == newtarget) {break} target = newtarget } // choose target if (aUseFirstChild.includes(k)) {target = parent.firstChild} // set writing-mode and style let extraStyle = undefined == oExtraStyles[k] ? '' : oExtraStyles[k] target.setAttribute('style','display:inline; writing-mode: '+ s + extraStyle +';') method = measureFn(target, METRIC) // typecheck itemdata = [method.width, method.height, method.x, method.y] if (isFirst) { isFirst = false if (undefined !== method.error) {throw method.errorstring} itemdata.forEach(function(item){ if (runST) {item = null} let typeCheck = typeFn(item) if ('number' !== typeCheck) {throw zErrType + typeCheck} }) } } catch(e) { itemdata = zErr log_error(15, METRIC +'_'+k, e) } let itemhash = mini(itemdata) if (undefined == tmpdata[style][itemhash]) {tmpdata[style][itemhash] = {'data': itemdata, 'group': [k]} } else {tmpdata[style][itemhash]['group'].push(k)} } } // group by results let newobj = {} for (const s of Object.keys(tmpdata)) { newobj[s] = {} for (const k of Object.keys(tmpdata[s])) { let keydata = tmpdata[s][k].group.sort() newobj[s][keydata.join(' ')] = tmpdata[s][k]['data'] } } for (const s of Object.keys(newobj)) { data[s] = {} for (const k of Object.keys(newobj[s]).sort()) {data[s][k] = newobj[s][k]} } hash = mini(data); btn = addButton(15, METRIC) } catch(e) { hash = e; data = zErrLog } removeElementFn(id) addBoth(15, METRIC, hash, btn,'', data, isLies) log_perf(15, METRIC, t0) return } function get_element_scrollbars(METRIC, isLies) { // https://bugzilla.mozilla.org/show_bug.cgi?id=1786665 // ui.useOverlayScrollbars: 0 = yes, 1 = no // widget.non-native-theme.scrollbar.size.override <-- non-overlay only in css pixels at full zoom (default 0) // this bypasses TB and changes auto + thin // widget.non-native-theme.scrollbar.style = values 0 to 5 (default, mac, gtk, android, win10, win11) // values 1,2,3 bypass TB and change both sizes // layout.css.scrollbar-width-thin.disabled = true // this bypasses TB and changes thin to match auto // widget.non-native-theme.win.scrollbar.use-system-size = boolean // FF143+ layout.testing.scrollbars.always-hidden has no effect on measurements // maybe it only affects the viewport? let oData = {'auto': {}, 'thin': {}} let aAuto = [], aThin = [], aWindow = [] let list = ['auto','thin'] // scrollWidth function get_scroll() { let element list.forEach(function(p) { // element let value, item = 'element' try { element = dom.tzpScroll element.style['scrollbar-width'] = p let target = element.children[0] let width, method method = measureFn(target, METRIC) if (undefined !== method.error) {throw method.errorstring} width = method.width if (runST) {width = NaN} else if (runSI) {width = 101} let typeCheck = typeFn(width) if ('number' !== typeCheck) {throw zErrType + typeCheck} value = 100 - width // 100 set in html, not affected by zoom if (value < 0) {throw zErrInvalid + '< 0'} // get scrollbar width for json overlay if (undefined == isScrollbar && 'auto' == p) {isScrollbar = value} } catch(e) { value = zErr log_error(15, METRIC +'_'+ p +'_'+ item, e) } let fpvalue = value if (isLies && zErr !== value) { value = log_known(15, METRIC +'_'+ p +'_'+ item, value) fpvalue = zLIE } oData[p][item] = fpvalue if ('auto' == p) {aAuto.push(value)} else {aThin.push(value)} // scrollWidth value = undefined, item = 'scrollWidth' try { element.style['scrollbar-width'] = p value = 100 - element.scrollWidth if (runST) {value = NaN} else if (runSI) {value = -1} let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} if (value < 0) {throw zErrInvalid + '< 0'} // get scrollbar width for json overlay if (undefined == isScrollbar && 'auto' == p) {isScrollbar = value} } catch(e) { value = zErr log_error(15, METRIC +'_'+ p +'_'+ item, e) } oData[p][item] = value if ('auto' == p) {aAuto.push(value)} else {aThin.push(value)} }) } get_scroll() if (undefined == isScrollbar) {isScrollbar = 20} addDisplay(15, METRIC, dedupeArray(aAuto, true) +' | '+ dedupeArray(aThin, true)) addData(15, METRIC, oData, mini(oData)) return } const outputElements = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('elements')) {return resolve()} let isLies = isDomRect == -1 Promise.all([ get_domrect('domrect'), get_element_font('element_font', isLies), get_element_keys('htmlelement_keys'), get_element_forms('element_forms', isLies), get_element_mathml('element_mathml', isLies), get_element_other('element_other', isLies), get_element_scrollbars('element_scrollbars', isLies), ]).then(function(){ return resolve() }) }) countJS(15) ================================================ FILE: js/fonts.js ================================================ 'use strict'; let fntCodes = [ // sorted // actualBoundingBox, width '0x007F','0x0218','0x058F','0x05C6','0x061C','0x0700','0x08E4','0x097F','0x09B3', '0x0B82','0x0D02','0x10A0','0x115A','0x17DD','0x1950','0x1C50','0x1CDA','0x1D790', '0x1E9E','0x20B0','0x20B8','0x20B9','0x20BA','0x20BD','0x20E3','0x21E4','0x23AE', '0x2425','0x2581','0x2619','0x2B06','0x2C7B','0x302E','0x3095','0x532D','0x6E2F', '0xA73D','0xA830','0xF003','0xF810','0xFBEE', /* ignore: https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character problematic e.g windows 1st use '0xFFF9','0xFFFD', //*/ '0xFFFF', ] let fntData = {}, fntSize = '512px', fntString = '-\uffff', fntBtn ='', fntFake, fntDocEnabled = false, fntBase = {}, fntBaseInvalid = {}, fntBaseMin = [], fntPlatformFont, // undefined fntHealth = [] let fntMaster = { // android core noto android: { notoboth: [ // sans: 5+ except CJK JP + Gujarati + Gurmukhi + Tibetan 9+ // serifs: 9+ except tibetan 12+ 'Armenian','Bengali','CJK JP','Devanagari','Ethiopic','Georgian','Gujarati','Gurmukhi','Hebrew','Kannada', 'Khmer','Lao','Malayalam','Myanmar','Sinhala','Tamil','Telugu','Thai','Tibetan'], notosans: [ // 5+ 'Bengali UI','Devanagari UI','Khmer UI','Lao UI','Malayalam UI','Myanmar UI','Symbols','Tamil UI','Telugu UI','Thai UI', // 9+ 'Adlam','Ahom','Anatolian Hieroglyphs','Avestan','Balinese','Bamum','Bassa Vah','Batak','Bhaiksuki','Brahmi','Buginese','Buhid', 'Canadian Aboriginal','Carian','Chakma','Cham','Cherokee','Coptic','Cuneiform','Cypriot','Deseret','Egyptian Hieroglyphs', 'Elbasan','Glagolitic','Gothic','Gujarati UI','Gurmukhi UI','Hanunoo','Hatran','Imperial Aramaic','Inscriptional Pahlavi', 'Inscriptional Parthian','Javanese','Kaithi','Kayah Li','Kharoshthi','Lepcha','Limbu','Linear A','Linear B','Lisu','Lycian', 'Lydian','Mandaic','Manichaean','Marchen','Meetei Mayek','Meroitic','Miao','Mongolian','Mro','Multani','Nabataean','Newa', 'New Tai Lue','NKo','Ogham','Ol Chiki','Old Italic','Old North Arabian','Old Permic','Old Persian','Old South Arabian', 'Old Turkic','Oriya','Oriya UI','Osage','Osmanya','Pahawh Hmong','Palmyrene','Pau Cin Hau','Phags Pa','Phoenician','Rejang', 'Runic','Samaritan','Saurashtra','Sharada','Shavian','Sinhala UI','Sora Sompeng','Sundanese','Syloti Nagri','Syriac Eastern', 'Syriac Estrangela','Syriac Western','Tagalog','Tagbanwa','Tai Le','Tai Tham','Tai Viet','Thaana','Tifinagh','Ugaritic','Vai','Yi', // 12+ 'Grantha','Gunjala Gondi','Hanifi Rohingya','Khojki','Masaram Gondi','Medefaidrin','Modi','Soyombo','Takri','Wancho','Warang Citi'], notoserif: ['Dogra','Nyiakeng Puachue Hmong','Yezidi'], // 12+ }, // BB13+ bundled: reuse for android/linux bundled: { // all: win/mac/linux: 80sans-only 4serif-only 17both: total 118, sorted hash: 8949a424 notoboth: [ 'Balinese','Bengali','Devanagari','Ethiopic','Georgian','Grantha','Gujarati','Gurmukhi','Kannada','Khmer', 'Khojki','Lao','Malayalam','Sinhala','Tamil','Telugu'], notosans: [ 'Adlam','Bamum','Bassa Vah','Batak','Buginese','Buhid','Canadian Aboriginal','Chakma','Cham','Cherokee', 'Coptic','Deseret','Elbasan','Gunjala Gondi','Hanifi Rohingya','Hanunoo','Javanese','Kayah Li','Khudawadi', 'Lepcha','Limbu','Lisu','Mahajani','Mandaic','Masaram Gondi','Medefaidrin','Meetei Mayek','Mende Kikakui', 'Miao','Modi','Mongolian','Mro','Multani','NKo','New Tai Lue','Newa','Ol Chiki','Oriya','Osage','Osmanya', 'Pahawh Hmong','Pau Cin Hau','Rejang','Runic','Samaritan','Saurashtra','Sharada','Shavian','Sora Sompeng', 'Soyombo','Sundanese','Syloti Nagri','Symbols','Symbols 2','Syriac','Tagalog','Tagbanwa','Tai Le','Tai Tham', 'Tai Viet','Takri','Thaana','Tifinagh','Tifinagh APT','Tifinagh Adrar','Tifinagh Agraw Imazighen', 'Tifinagh Ahaggar','Tifinagh Air','Tifinagh Azawagh','Tifinagh Ghat','Tifinagh Hawad','Tifinagh Rhissa Ixa', 'Tifinagh SIL','Tifinagh Tawellemmet','Tirhuta','Vai','Wancho','Warang Citi','Yi','Zanabazar Square'], notoserif: ['Dogra','Myanmar','NP Hmong','Tibetan','Yezidi'], android: [], // notos then linux +16, mac +5, win +4 linux: [ 'Arimo','Cousine','Noto Color Emoji','Noto Naskh Arabic','Noto Sans Armenian','Noto Sans Hebrew','Noto Sans JP', 'Noto Sans KR','Noto Sans SC','Noto Sans TC','Noto Sans Thai','Noto Serif Armenian','Noto Serif Hebrew', 'Noto Serif Thai','Pyidaungsu','STIX Two Math','Tinos','Twemoji Mozilla', ], mac: ['Noto Sans Armenian','Noto Sans Hebrew','Noto Serif Armenian','Noto Serif Hebrew','Pyidaungsu','STIX Two Math'], windows: ['Noto Naskh Arabic','Noto Sans','Noto Serif','Pyidaungsu','Twemoji Mozilla'], }, // BB whitelist system allowlist: { android: [], linux: [ 'Arial','Courier','Courier New','Helvetica','Times','Times New Roman' // aliases ], linuxfaces: [ // bundled 'Arimo Regular','Cousine','Cousine Regular','Noto Sans JP','Noto Sans Symbols Regular','Tinos','Tinos Regular', ], mac: [ // 10.15+ 'AppleGothic','Arial','Courier New','Geneva','Georgia','Heiti TC', 'Helvetica','Helvetica Neue','Kailasa','Lucida Grande','Menlo','Monaco','PingFang HK','PingFang SC', 'PingFang TC','Songti SC','Songti TC','Tahoma','Thonburi','Times New Roman','Verdana', // other 'Apple Color Emoji', // listed as 11+ but likely also on 10.15 '-apple-system', // always // 11- | legacy // https://searchfox.org/mozilla-central/rev/f53c09a22edc700ab1a9eaaf4da0f0dd9f11bff3/gfx/thebes/CoreTextFontList.cpp#38-44 'Courier','Times', // ToDo: document-supported only 'Hiragino Kaku Gothic ProN', //'ヒラギノ角ゴ', // mac doesn't seem to do localized // ToDo: these are weighted but seem to test correctly in mac but I don't like weighted fonts here // These are listed in faces: maybe remove the them from here once fontface is enabled in TB 'Arial Black','Arial Narrow', ], macfaces: [ // 10.15+ 'Arial Black', 'Arial Bold','Arial Bold Italic','Arial Italic', 'Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic', 'Courier New Bold','Courier New Bold Italic','Courier New Italic', 'Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Heiti TC Light','Heiti TC Medium', 'Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Oblique', 'Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Italic','Helvetica Neue Light', 'Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin', 'Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic', 'Kailasa Bold', 'Lucida Grande Bold', 'Menlo Bold','Menlo Bold Italic','Menlo Italic', 'PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight', 'PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight', 'PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Songti SC Black','Songti SC Bold','Songti SC Light', 'Songti TC Bold','Songti TC Light', 'Tahoma Bold', 'Thonburi Bold','Thonburi Light', 'Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Verdana Bold','Verdana Bold Italic','Verdana Italic', // problematic 'Courier Bold','Courier Bold Oblique','Courier Oblique', // 11 or lower | legacy 'Times Bold','Times Bold Italic','Times Italic', // 11 or lower // non weight/style 'AppleGothic Regular','Geneva','Monaco', // 10.15+ 'Apple Color Emoji', // 11+ 'Noto Sans Hebrew Regular','Noto Serif Armenian Regular', // bundled not in macOS/kBaseFonts // ToDo: document-supported only 'Hiragino Kaku Gothic ProN W3','Hiragino Kaku Gothic ProN W6', ], windows: [ // 7 'Arial','Cambria Math','Consolas','Courier New','Georgia','Lucida Console','MS Gothic','MS ゴシック', 'MS PGothic','MS Pゴシック','MV Boli','Malgun Gothic','맑은 고딕','Microsoft Himalaya','Microsoft JhengHei','微軟正黑體', 'Microsoft YaHei','微软雅黑','Segoe UI','SimSun','宋体','Sylfaen','Tahoma','Times New Roman','Verdana', // system aliases // https://searchfox.org/mozilla-central/source/gfx/thebes/gfxDWriteFontList.cpp#1990 // should always be the same but lets test everything in BB 'MS Serif','Courier','Small Fonts','Roman', // TNR, Courier New, Arial, TNR // fntPlatformFont 'MS Shell Dlg \\32', // FontSubstitutes // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontSubstitutes // BB FontSubstitutes -> whitelisted 'Arabic Transparent','Arial (Arabic)','Arial (Hebrew)','Arial Baltic','Arial CE','Arial CYR', 'Arial Greek','Arial TUR','Courier New (Hebrew)','Courier New Baltic','Courier New CE', 'Courier New CYR','Courier New Greek','Courier New TUR','Helvetica','MS Shell Dlg 2','Tahoma Armenian', 'Times','Times New Roman (Hebrew)','Times New Roman Baltic','Times New Roman CE','Times New Roman CYR', 'Times New Roman Greek','Times New Roman TUR','Tms Rmn','MS Serif Greek','Small Fonts Greek', '標準ゴシック','ゴシック','ゴシック', // MS ゴシック -> MS Gothic 'ヘルベチカ','タイムズロマン','クーリエ', // Arial, TNR, Courier -> Courier New // see 44461: FF135+ windows 10 UX issues (1947324) fixed in FF147 (1996961) 'Segoe MDL2 Assets', ], windowsfaces: [ // weighted/styles 'Arial Black','Arial Bold','Arial Bold Italic','Arial Italic', 'Consolas Bold','Consolas Bold Italic','Consolas Italic', 'Courier New Bold','Courier New Bold Italic','Courier New Italic', 'Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Malgun Gothic Bold','Malgun Gothic Semilight', 'Microsoft JhengHei Bold','Microsoft JhengHei Light', 'Microsoft YaHei Bold','Microsoft YaHei Light', 'Segoe UI Black','Segoe UI Black Italic','Segoe UI Bold','Segoe UI Bold Italic', 'Segoe UI Italic','Segoe UI Light','Segoe UI Light Italic','Segoe UI Semibold', 'Segoe UI Semibold Italic','Segoe UI Semilight','Segoe UI Semilight Italic', 'Tahoma Bold', 'Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Verdana Bold','Verdana Bold Italic','Verdana Italic', // other 'Cambria Math','Lucida Console','MS Gothic', // system 'Noto Sans Gujarati Regular','Noto Serif Dogra Regular','Twemoji Mozilla', // bundled ], windowsoffscreen: [ // proportional only: in BB we test once against monospace + fallbacks 'Arial','Cambria Math','MS PGothic','Malgun Gothic','Microsoft JhengHei','Microsoft YaHei', 'Segoe UI','Sylfaen','Times New Roman','Verdana', // some windows-only bundled fonts + twemoji 'Noto Naskh Arabic','Noto Sans','Noto Serif','Twemoji Mozilla', ], }, // BB unexpected blocklist: { android: [], linux: [ 'Noto Emoji','Noto Mono','Noto Sans','Noto Serif', // notos 'Cantarell','DejaVu Sans','DejaVu Serif','Droid Sans','STIX', // fedora 'Dingbats','FreeMono','Ubuntu', // ubuntu 'Bitstream Charter','C059','Nimbus Sans','P052','Quicksand', // debian 'Liberation Mono','Liberation Sans','Liberation Serif', // popular 'Noto Serif Hmong Nyiakeng','Noto Sans Symbols2','STIX Math', // BB12 fontnames 'Noto Sans Myanmar', // BB14.5 bundled replaced by Pyidaungsu ], linuxfaces: [ 'Arimo', // Arimo without regular seems not to work, double check it. 'Arial','Arial Regular','Courier New','Courier New Regular', // aliases, expected not to work! // common linux fonts 'Cantarell Regular','DejaVu Sans', // fedora 'Ubuntu', // ubuntu 'Nimbus Sans','Nimbus Sans Regular', // debian 'FreeSans','Liberation Sans', // popular // expand: ^ above the only NEW font tested is FreeSans 'Symbola', // fedora 'Jamrul','Kinnari', // ubuntu 'OpenSymbol', // openoffice 'Amiri', // libreoffice 'Bitstream Charter' // debian ], mac: [ 'Apple Symbols','Impact','Palatino','Rockwell', // system 'Noto Serif Hmong Nyiakeng','STIX Math', // BB12 fontnames 'Noto Sans Symbols2', // BB12 bundled: ToDo: in faces: maybe remove when that lands in TB 'Noto Sans Myanmar', // BB14.5 bundled replaced by Pyidaungsu '.Helvetica Neue DeskInterface', // dot-prefixed font families on mac = hidden // tb#42377 ], macfaces: [ // weighted 'Avenir Light','Charter Black','Damascus Medium','Gill Sans Light','Hiragino Sans W3', // other 'Apple Braille','Trebuchet MS','Webdings','Wingdings','Zapfino', // system 'Noto Sans Symbols2', // BB12 bundled ], windows: [ 'Calibri','Candara','Microsoft JhengHei UI','MS UI Gothic','Microsoft YaHei UI','NSimSun','Segoe UI Emoji','SimSun-ExtB', // system 'MS Shell Dlg', // system alias == Microsoft Sans Serif 'Gill Sans','Gill Sans MT', // MS bundled // other 'Noto Sans Symbols2', // BB12 bundled 'Noto Sans Myanmar', // BB14.5 bundled replaced by Pyidaungsu 'Noto Color Emoji', // bundled nightly: should be removed 'Helv', // ToDo: this might need to move with font.vis ], windowsfaces: [ // weighted // 'Arial Narrow', // ToDo: uncomment once we block it 'Calibri Light', // 8 'Microsoft JhengHei UI Light','Nirmala UI Semilight', // 8.1 'Candara Light','Corbel Light','Yu Gothic UI Light', // 10 // other 'MS UI Gothic','Microsoft YaHei UI','NSimSun','SimSun-ExtB', // system 'Gill Sans','Gill Sans MT', // MS bundled 'Noto Serif Hmong Nyiakeng Regular', // BB12 bundled 'Arabic Transparent', // fontSubstitutes 'Courier','MS Serif','Roman', // aliases ], windowsoffscreen: [ // proportional only: in BB we test once against monospace + fallbacks 'Cambria','Comic Sans MS','Gabriola','Impact','Webdings', // system 'MS Shell Dlg', // system alias == Microsoft Sans Serif 'Gill Sans','Gill Sans MT', // MS bundled // other 'Noto Sans Symbols2', // BB12 bundled ], }, // kBaseFonts: https://searchfox.org/mozilla-central/search?path=StandardFonts*.inc base: { android: [], linux: [], mac: [ // always '-apple-system', //kBaseFonts 'Al Bayan','Al Nile','Al Tarikh','American Typewriter','Andale Mono','Apple Braille','Apple Chancery', 'Apple Color Emoji','Apple SD Gothic Neo','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Hebrew','Arial Hebrew Scholar','Arial Unicode MS','Avenir Next','Ayuthaya', 'Baghdad','Bangla MN','Bangla Sangam MN','Baskerville','Beirut','Bodoni Ornaments', 'Bodoni 72','Bodoni 72 Oldstyle','Bodoni 72 Smallcaps', // bodoni 72 font-family we drop 'book' 'Catamaran','Chalkboard','Chalkboard SE','Chalkduster','Cochin','Comic Sans MS','Copperplate', 'Corsiva Hebrew','Courier New', 'Damascus','DecoType Naskh','Devanagari MT','Devanagari Sangam MN','Didot','Diwan Kufi','Diwan Thuluth', 'Euphemia UCAS', 'Farah','Farisi', 'GB18030 Bitmap','Galvji','Geeza Pro','Geneva','Georgia','Gill Sans','Gujarati MT','Gujarati Sangam MN', 'Gurmukhi MN','Gurmukhi MT','Gurmukhi Sangam MN', 'Helvetica','Helvetica Neue','Hoefler Text','Hiragino Maru Gothic ProN', 'Impact','InaiMathi', 'Kailasa','Kannada MN','Kannada Sangam MN','Kefa','Khmer MN','Khmer Sangam MN','Kohinoor Bangla', 'Kohinoor Devanagari','Kohinoor Gujarati','Kohinoor Telugu','Kokonor','Krungthep','KufiStandardGK', 'Lao MN','Lao Sangam MN','Lucida Grande','Luminari', 'Malayalam MN','Malayalam Sangam MN','Menlo','Microsoft Sans Serif','Mishafi','Mishafi Gold', 'Monaco','Mshtakan','Mukta Mahee','Muna','Myanmar MN','Myanmar Sangam MN', 'Nadeem','New Peninim MT', 'Noto Nastaliq Urdu', 'Noto Sans Adlam','Noto Sans Armenian','Noto Sans Avestan','Noto Sans Bamum','Noto Sans Bassa Vah', 'Noto Sans Batak','Noto Sans Bhaiksuki','Noto Sans Brahmi','Noto Sans Buginese','Noto Sans Buhid', 'Noto Sans Canadian Aboriginal','Noto Sans Carian','Noto Sans Caucasian Albanian','Noto Sans Chakma', 'Noto Sans Cham','Noto Sans Coptic','Noto Sans Cuneiform','Noto Sans Cypriot','Noto Sans Duployan', 'Noto Sans Egyptian Hieroglyphs','Noto Sans Elbasan','Noto Sans Glagolitic','Noto Sans Gothic', 'Noto Sans Gunjala Gondi','Noto Sans Hanifi Rohingya','Noto Sans Hanunoo','Noto Sans Hatran', 'Noto Sans Imperial Aramaic','Noto Sans Inscriptional Pahlavi','Noto Sans Inscriptional Parthian', 'Noto Sans Javanese','Noto Sans Kaithi','Noto Sans Kannada','Noto Sans Kayah Li','Noto Sans Kharoshthi', 'Noto Sans Khojki','Noto Sans Khudawadi','Noto Sans Lepcha','Noto Sans Limbu','Noto Sans Linear A', 'Noto Sans Linear B','Noto Sans Lisu','Noto Sans Lycian','Noto Sans Lydian','Noto Sans Mahajani', 'Noto Sans Mandaic','Noto Sans Manichaean','Noto Sans Marchen','Noto Sans Masaram Gondi', 'Noto Sans Meetei Mayek','Noto Sans Mende Kikakui','Noto Sans Meroitic','Noto Sans Miao','Noto Sans Modi', 'Noto Sans Mongolian','Noto Sans Mro','Noto Sans Multani','Noto Sans Myanmar','Noto Sans Nabataean', 'Noto Sans New Tai Lue','Noto Sans Newa','Noto Sans NKo','Noto Sans Ol Chiki','Noto Sans Old Hungarian', 'Noto Sans Old Italic','Noto Sans Old North Arabian','Noto Sans Old Permic','Noto Sans Old Persian', 'Noto Sans Old South Arabian','Noto Sans Old Turkic','Noto Sans Oriya','Noto Sans Osage','Noto Sans Osmanya', 'Noto Sans Pahawh Hmong','Noto Sans Palmyrene','Noto Sans Pau Cin Hau','Noto Sans PhagsPa','Noto Sans Phoenician', 'Noto Sans Psalter Pahlavi','Noto Sans Rejang','Noto Sans Samaritan','Noto Sans Saurashtra','Noto Sans Sharada', 'Noto Sans Siddham','Noto Sans Sora Sompeng','Noto Sans Sundanese','Noto Sans Syloti Nagri','Noto Sans Syriac', 'Noto Sans Tagalog','Noto Sans Tagbanwa','Noto Sans Tai Le','Noto Sans Tai Tham','Noto Sans Tai Viet', 'Noto Sans Takri','Noto Sans Thaana','Noto Sans Tifinagh','Noto Sans Tirhuta','Noto Sans Ugaritic', 'Noto Sans Vai','Noto Sans Wancho','Noto Sans Warang Citi','Noto Sans Yi','Noto Sans Zawgyi', 'Noto Serif Ahom','Noto Serif Balinese','Noto Serif Hmong Nyiakeng','Noto Serif Myanmar','Noto Serif Yezidi', 'Optima','Oriya MN','Oriya Sangam MN', 'PT Mono','PT Sans','PT Sans Caption','PT Serif','PT Serif Caption','Palatino','Papyrus', 'PingFang HK','PingFang SC','PingFang TC','Plantagenet Cherokee', 'Raanana','Rockwell', 'STIX Two Math', // FF133+ 1902570 'STIX Two Math Regular', // 2000429 'STIXGeneral','STIXIntegralsD','STIXIntegralsSm','STIXIntegralsUp','STIXIntegralsUpD','STIXIntegralsUpSm', 'STIXNonUnicode','STIXSizeFiveSym','STIXSizeFourSym','STIXSizeOneSym','STIXSizeThreeSym','STIXSizeTwoSym', 'STIXVariants','STSong','Sana','Sathu','Savoye LET','Shree Devanagari 714','SignPainter','Silom','Sinhala MN', 'Sinhala Sangam MN','Skia','Snell Roundhand','Songti SC','Songti TC','Symbol', 'Tahoma','Tamil MN','Tamil Sangam MN','Telugu MN','Telugu Sangam MN','Thonburi','Times New Roman', 'Trattatello','Trebuchet MS', 'Verdana', 'Waseem','Webdings','Wingdings','Wingdings 2','Wingdings 3', 'Zapf Dingbats','Zapfino', // 11- | legacy 'Courier','Times', // localized: just in case: ToDo: test JAP install '애플고딕', // AppleGothic '애플명조', // AppleMyungjo 'ヒラギノ丸ゴ', // Hiragino Maru Gothic ProN ], macfaces: [ // weighted/styled 'Arial Black','Arial Rounded MT Bold','Arial Narrow','PT Sans Narrow', // without the weight W* it loads W3 'Hiragino Sans W4', // works in font-family if we drop 'book'/'roman' 'Avenir Book','Avenir Book Oblique','Avenir Roman','ITF Devanagari Book','ITF Devanagari Marathi Book', // legacy 'Courier','Courier Bold','Courier Bold Oblique','Courier Oblique', 'Times','Times Bold','Times Bold Italic','Times Italic','Times Roman', // these ones have no regular/normal 'Avenir Black','Avenir Black Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light', 'Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Oblique', 'Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Demi Bold','Avenir Next Heavy', 'Avenir Next Medium','Avenir Next Ultra Light', 'Big Caslon Medium','Bradley Hand Bold','Brush Script MT Italic', 'Charter Black','Charter Bold', //'Charter Black Italic','Charter Bold Italic','Charter Italic', 'DIN Alternate Bold','DIN Condensed Bold', 'Futura Condensed ExtraBold','Futura Condensed Medium', 'Futura Medium', //'Futura Bold','Futura Medium Italic', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Hiragino Mincho ProN W3','Hiragino Mincho ProN W6', 'Hiragino Sans GB W3','Hiragino Sans GB W6', 'ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Medium',//'ITF Devanagari Bold', 'ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium',//'ITF Devanagari Marathi Bold', 'Marker Felt Thin','Marker Felt Wide', 'Noteworthy Bold','Noteworthy Light', 'Phosphate Inline','Phosphate Solid', 'Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Text','Sukhumvit Set Thin', //'Sukhumvit Set Bold','Sukhumvit Set Semi Bold', // SyntaxError: An invalid or illegal string was specified // 'Bodoni 72 Bold','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book Italic', ], windows: [ // 7 'Arial','Calibri','Cambria','Cambria Math','Candara','Comic Sans MS','Consolas','Constantia','Corbel','Courier New', 'Ebrima','Gabriola','Georgia','Impact','Lucida Console','Lucida Sans Unicode','MS Gothic','MS ゴシック', 'MS PGothic','MS Pゴシック','MS UI Gothic','MV Boli','Malgun Gothic','맑은 고딕','Marlett','Microsoft Himalaya', 'Microsoft JhengHei','微軟正黑體','Microsoft New Tai Lue','Microsoft PhagsPa','Microsoft Sans Serif', 'Microsoft Tai Le','Microsoft YaHei','微软雅黑','Microsoft Yi Baiti','MingLiU-ExtB','細明體-ExtB', 'MingLiU_HKSCS-ExtB','細明體_HKSCS-ExtB','Mongolian Baiti','NSimSun','新宋体','PMingLiU-ExtB','新細明體-ExtB', 'Palatino Linotype','Segoe Print','Segoe Script','Segoe UI','Segoe UI Symbol','SimSun','宋体','SimSun-ExtB', 'Sylfaen','Symbol','Tahoma','Times New Roman','Trebuchet MS','Verdana','Webdings','Wingdings', // 8 'Gadugi','Nirmala Text','Nirmala UI','Microsoft JhengHei UI','Microsoft YaHei UI','Myanmar Text', // 8.1 'Javanese Text','Leelawadee UI','Segoe UI Emoji','Sitka Banner','Sitka Display', 'Sitka Heading','Sitka Small','Sitka Subheading','Sitka Text','Yu Gothic','游ゴシック', // 10 'Bahnschrift','Segoe MDL2 Assets','Segoe UI Historic','Yu Gothic UI', // 11 'SimSun-ExtG', // win11 24H2 see 1947960 | 1954265 FF139 adeed to base // fntPlatformFont 'MS Shell Dlg \\32', // common FontSubstitutes that point to kBase fonts 'MS Shell Dlg','MS Shell Dlg 2', // can differ // FontSubstitutes which can be or are aliased to kBaseFonts // 1845105: FF141+ 'Arial (Arabic)','Arial (Hebrew)','Arabic Transparent','Arial Baltic','Arial CE','Arial CYR','Arial Greek','Arial TUR', 'Courier','Courier New (Arabic)','Courier New (Hebrew)','Courier New Baltic','Courier New CE','Courier New CYR', 'Courier New Greek','Courier New TUR','Helv','Helvetica','MS Serif Greek','MS Sans Serif Greek','Small Fonts Greek', 'Tahoma Armenian','Times','Times New Roman (Arabic)','Times New Roman (Hebrew)','Times New Roman Baltic', 'Times New Roman CE','Times New Roman CYR','Times New Roman Greek','Times New Roman TUR','Tms Rmn', // jap 'ヘルベチカ', // arial 'クーリエ', // Courier New '標準ゴシック','ゴシック','ゴシック', // MS ゴシック -> MS Gothic 'タイムズロマン', // TNR /* ignore // system aliases: should always be the same AFAICT // https://searchfox.org/mozilla-central/source/gfx/thebes/gfxDWriteFontList.cpp#1990 'MS Sans Serif','MS Serif','Small Fonts','Roman', // Microsoft Sans Serif, TNR, Arial, TNR //'Franklin Gothic Medium', // 7 not detected if font-vis < 3: 1720408 //*/ ], windowsfaces: [ // weighted 'Arial Black','Arial Narrow','Segoe UI Light','Segoe UI Semibold', // 7 'Calibri Light','Segoe UI Semilight', // 8 // 8.1 'Leelawadee UI Semilight','Microsoft JhengHei Light','Microsoft JhengHei UI Light', 'Microsoft YaHei Light','Microsoft YaHei UI Light','Nirmala UI Semilight','Segoe UI Black','Yu Gothic Light', // 10 'Candara Light','Corbel Light','Malgun Gothic Semilight', 'Yu Gothic Medium','Yu Gothic UI Light','Yu Gothic UI Semilight','Yu Gothic UI Semibold', /* ignore: not detected by font face 'Bahnschrift Light','Bahnschrift SemiBold','Bahnschrift SemiLight', //*/ ], windowsoffscreen: [ // vs MS Shell Dlg \32 'Calibri','Cambria','Georgia','MV Boli','Marlett','NSimSun','Sylfaen', ] }, // kLangPackFonts baselang: { android: [], linux: [], mac: [], windows: [ 'Aharoni','Aldhabi','Andalus','Angsana New','AngsanaUPC','Aparajita','Arabic Typesetting','BIZ UDGothic','BIZ UDゴシック', 'BIZ UDMincho','BIZ UD明朝','BIZ UDPGothic','BIZ UDPゴシック','BIZ UDPMincho','BIZ UDP明朝','Batang','바탕','BatangChe','바탕체', 'Browallia New','BrowalliaUPC','Cordia New','CordiaUPC','DFKai-SB','DaunPenh','David','DengXian','等线','DilleniaUPC', 'DokChampa','Dotum','돋움','DotumChe','돋움체','Estrangelo Edessa','EucrosiaUPC','Euphemia','FangSong','仿宋','FrankRuehl', 'FreesiaUPC','Gautami','Gisha','Gulim','굴림','GulimChe','굴림체','Gungsuh','궁서','GungsuhChe','궁서체','IrisUPC','Iskoola Pota', 'JasmineUPC','KaiTi','楷体','Kalinga','Kartika','Khmer UI','KodchiangUPC','Kokila','Lao UI','Latha','Leelawadee','Levenim MT', 'LilyUPC','MS Mincho','MS 明朝','MS PMincho','MS P明朝','Mangal','Meiryo','メイリオ','Meiryo UI','Microsoft Uighur', 'MingLiU','細明體','MingLiU_HKSCS','細明體_HKSCS','Miriam','Miriam Fixed','MoolBoran','Narkisim','Nyala','PMingLiU','新細明體', 'Plantagenet Cherokee','Raavi','Rod','Sakkal Majalla','Sanskrit Text','Shonar Bangla','Shruti','SimHei','黑体', 'Simplified Arabic','Traditional Arabic','Tunga','Urdu Typesetting','Utsaah','Vani','Vijaya','Vrinda','Yu Mincho','游明朝', // UD Digi // to save on code turmoil and maintenance, allow 24H2 in FF144 or lower: if FPP leaks it will show // with other fonts and if FPP doesn't leak then they simply will not show in the results 'UD Digi Kyokasho N-R','UD Digi Kyokasho NK-R','UD Digi Kyokasho NP-R', // original (ignore weighted -B bold) 'UD Digi Kyokasho N','UD Digi Kyokasho NK','UD Digi Kyokasho NP', // 24H2: FF145+ 1988407 // 1954265: FF139+ win11 23H2 / win1022H2 'Noto Sans HK','Noto Sans JP','Noto Sans KR','Noto Sans SC','Noto Sans TC', 'Noto Serif HK','Noto Serif JP','Noto Serif KR','Noto Serif SC','Noto Serif TC', // FontSubstitutes which can be or are aliased to kLangPackFonts // 1845105: FF141+ // hebrew 'David Transparent', // David 'Fixed Miriam Transparent', // Miriam Fixed 'Miriam Transparent', // Miriam 'Rod Transparent', // Rod // japanese '標準明朝', // MS 明朝 -> MS Mincho, // simplified chinese 'FangSong_GB2312', // FangSong 'KaiTi_GB2312','楷体_GB2312' // KaiTi + localized /* Fixedsys and system do not seem to be available to web content 'Fixedsys Greek', // Fixedsys 'System Greek', // System //*/ ], windowsfaces: [ 'BIZ UDMincho Medium','BIZ UDPMincho Medium','DengXian Light','Yu Mincho Demibold','Yu Mincho Light' ], windowsoffscreen: [ // to check if RFP/FPP leak, not determine RFP vs FPP: no gurantee of supplemental kLangPackFonts // add a token range of scripts in win10+ that were in win7/8 (more likely if user upgraded?) 'Aldhabi','Aparajita','Batang','David','Estrangelo Edessa','Euphemia','FangSong','Gautami','Iskoola Pota', 'Kalinga','Kartika','Khmer UI','Lao UI','Latha','Leelawadee','Meiryo','MingLiU','Nyala','Plantagenet Cherokee', 'Raavi','Shonar Bangla','Shruti','Tunga', ] }, // NOT kBase/LangPack system: { android: [ // common: note 'AndroidClock' ignored 'Carrois Gothic SC','Cutive Mono','Dancing Script','Droid Sans Mono','Noto Color Emoji','Noto Naskh Arabic', // all 'Coming Soon','Noto Naskh Arabic UI','Roboto', // 9+ 'Noto Color Emoji Flags','Source Sans Pro', // 12+ // other 'Droid Sans','Droid Sans Fallback','Droid Serif', 'Noto Sans','Noto Sans CJK KR','Noto Sans CJK SC','Noto Sans CJK TC','Noto Sans SC','Noto Sans TC', 'Noto Serif','Noto Serif CJK KR','Noto Serif CJK SC','Noto Serif CJK TC','Noto Serif SC','Noto Serif TC', // +samsung 'One UI Sans APP VF','One UI Sans KR VF', // 1865238 'SamsungColorEmoji', // 1872510 'SamsungKorean_v2.0','SamsungKorean_v3.0', // 1674683 'SamsungKhmerUI','SamsungMyanmarShan','SamsungMyanmarZawgyiUI', // +xiaomi: https://hyperos.mi.com/font/en/ 'MiSans','MiSans VF','MiSans TC','MiSans TC VF', // 1933410, 1954947 'MiSans Arabic', // for all these maybe append VF as more likely? or both? 'MiSans Devanagari', 'MiSans Khmer', 'MiSans Latin', 'MiSans Lao', 'MiSans Gujarati', 'MiSans Gurmukhi', 'MiSans Myanmar', 'MiSans Thai', 'MiSans Tibetan', // +SEC 'New SEC Num Fixed VF','New SEC Num VF','SEC Bengali','SEC Bengali UI','SEC CJK Regular Extra', 'SEC Devanagari','SEC Devanagari UI','SEC Lao','SEC Lao UI','SEC Malayalam','SEC Malayalam UI', 'SEC Myanmar UI','SEC Naskh Arabic','SEC Naskh Arabic UI','SEC Serif Tibetan','SEC Tamil','SEC Tamil UI', 'SECFallback','SECGujarati','SECGujarati UI','SECGurmukhi','SECGurmukhi UI','SECKannada','SECKannada UI', 'SECTelugu','SECTelugu UI', // +SEC CJK 'SEC CJK JP','SEC CJK KR','SEC CJK SC','SEC CJK TC','SEC Mono CJK JP','SEC Mono CJK KR','SEC Mono CJK SC', 'SEC Mono CJK TC', ], linux: [ // self 'Noto Sans','Noto Serif', // +always 'Arial','Courier','Courier New', // +common notos 'Noto Emoji','Noto Sans Tibetan', // +selective kBase ubuntu or fedora // notos 'Noto Color Emoji','Noto Mono','Noto Serif CJK JP','Noto Serif CJK KR','Noto Serif CJK SC','Noto Serif CJK TC', // western/symbols 'Cantarell','DejaVu Sans','DejaVu Serif','Droid Sans','STIX','STIX Two Text','Symbola', // fedora 'Dingbats','FreeMono','Jamrul','Kinnari','Ubuntu', // ubuntu // other 'OpenSymbol', // openoffice 'Amiri', // libreoffice 'Liberation Mono','Liberation Sans','Liberation Serif', // scripts // ubuntu 'KacstNaskh','PakType Naskh Basic', // arabic 'Likhan','Mitra Mono','Mukti Narrow', // bengali 'Chandas','Kalimati','Samanata', // devangari 'Gargi','Nakula','Rachana','Sahadeva','Sarai', 'Rekha','Saab','Samyak Gujarati', // gujarati 'Lohit Gurmukhi', // gurmukhi 'Gubbi','Navilu', // kannada 'Phetsarath OT', // lao 'Chilanka','Gayathri','Suruma', // malayalam 'Dyuthi','Karumbi','Keraleeyam','Manjari','Uroob', 'ori1Uni','utkal', // oriya 'LKLUG','padmaa', // sinhala 'Samyak Tamil', // tamil 'Pothana2000','Vemana2000', // telugu 'Laksaman','Purisa','Umpush', // thai 'Norasi','Tlwg Mono', 'Jomolhari', // tibetan, also chrome os 'Pagul','Rasa','Yrsa', // multiscript // fedora 'Droid Arabic Kufi', // arabic 'Droid Sans Devanagari', // devangari 'Droid Sans Ethiopic', // ethiopic 'Droid Sans Hebrew', // hebrew 'Droid Sans Japanese', // jap 'Khmer OS', // khmer 'Droid Sans Tamil', // tamil 'Droid Sans Thai', // thai 'Nuosu SIL', // yi // debian 'Bitstream Charter','C059','Courier 10 Pitch','D050000L', 'DejaVu Math TeX Gyre','Nimbus Mono PS','Nimbus Roman', 'Nimbus Sans','P052','Quicksand','Standard Symbols PS', 'URW Bookman','URW Gothic','Z003', // ToDo: expand 'Inter', ], mac: [ //NOT kBase/LangPack 'Academy Engraved LET','Adelle Sans Devanagari','AkayaKanadaka','AkayaTelivigala','Annai MN','Arima Koshi','Arima Madurai','Avenir Next Condensed', 'Bai Jamjuree','Baloo 2','Baloo Bhai 2','Baloo Bhaijaan','Baloo Bhaina 2','Baloo Chettan 2','Baloo Da 2','Baloo Paaji 2', 'Baloo Tamma 2','Baloo Tammudu 2','Baloo Thambi 2','Baoli SC','Baoli TC','BiauKai','BiauKaiHK','BiauKaiTC','BIZ UDGothic', 'BIZ UDMincho','BM DoHyeon','BM Hanna 11yrs Old','BM Hanna Air','BM Hanna Pro','BM Jua','BM Kirang Haerang','BM Yeonsung', 'Cambay Devanagari','Canela','Canela Deck','Canela Text','Chakra Petch','Charm','Charmonman', 'Dash','Domaine Display', 'Fahkwang','Founders Grotesk','Founders Grotesk Condensed','Founders Grotesk Text', 'Gotu','Grantha Sangam MN','Graphik','Graphik Compact','GungSeo', 'Hannotate SC','Hannotate TC','HanziPen SC','HanziPen TC','HeadLineA','Hei','Herculanum','Hubballi', 'Jaini','Jaini Purva', 'K2D','Kai','Kaiti SC','Kaiti TC','Katari','Kavivanar','Kigelia','Kigelia Arabic','Kodchasan','KoHo','Krub', 'Kefa III', // new tahoe 26 'Lahore Gurmukhi','Lava Devanagari','Lava Kannada','Lava Telugu', 'LiHei Pro','LiSong Pro','Libian SC','Libian TC', 'Maku','Mali','Modak','Mukta','Mukta Malar','Mukta Vaani','Myriad Arabic', 'Nanum Brush Script','Nanum Gothic','Nanum Myeongjo','나눔명조','Nanum Pen Script','Niramit','Nom Na Tong', 'Noto Serif Kannada','November Bangla Traditional', 'October Compressed Devanagari','October Compressed Gujarati','October Compressed Gurmukhi','October Compressed Kannada', 'October Compressed Meetei Mayek','October Compressed Odia','October Compressed Ol Chiki','October Compressed Tamil','October Compressed Telugu', 'October Condensed Devanagari','October Condensed Gujarati','October Condensed Gurmukhi','October Condensed Kannada', 'October Condensed Meetei Mayek','October Condensed Odia','October Condensed Ol Chiki','October Condensed Tamil','October Condensed Telugu', 'October Devanagari','October Gujarati','October Gurmukhi','October Kannada', 'October Meetei Mayek','October Odia','October Ol Chiki','October Tamil','October Telugu', 'Osaka','Osaka-Mono', 'Padyakke Expanded One','Party LET','PCMyungjo','PilGi','PingFang MO','Produkt','Proxima Nova','PSL Ornanong Pro', 'Quotes Caps','Quotes Script', 'Sama Devanagari','Sama Gujarati','Sama Gurmukhi','Sama Kannada','Sama Malayalam','Sama Tamil','Sarabun','Sauber Script', 'Shobhika','SimSong','Spot Mono','Srisakdi','STFangsong','STHeiti','STIX Two Text','STKaiti','华文楷体','STXihei', 'Tiro Bangla','Tiro Devanagari Hindi','Tiro Devanagari Marathi','Tiro Devanagari Sanskrit','Tiro Gurmukhi','Tiro Kannada','Tiro Tamil', 'Tiro Telugu','Toppan Bunkyu Gothic','Toppan Bunkyu Mincho','Tsukushi A Round Gothic','Tsukushi B Round Gothic', 'Wawati SC','Wawati TC', 'Yuanti SC','Yuanti TC','Yuppy SC','Yuppy TC', // legacy // https://searchfox.org/mozilla-central/rev/f53c09a22edc700ab1a9eaaf4da0f0dd9f11bff3/gfx/thebes/CoreTextFontList.cpp#38-44 'Athelas','Marion','Seravek','Superclarendon', // downloadable but I have it 'Iowan Old Style', // NFI 'BlinkMacSystemFont', ], macfaces: [ 'Avenir Next Condensed Bold','Avenir Next Condensed Demi Bold', 'Avenir Next Condensed Heavy','Avenir Next Condensed Medium','Avenir Next Condensed Ultra Light', 'Brill Roman Bold','Brill Roman Medium','Brill Roman Semibold', // works in font-family if we drop 'roman' 'Brill Roman','Publico Headline Roman','Publico Text Roman', // weighted/styled: all these ones have no regular/normal 'Apple LiGothic Medium','Apple LiSung Light', 'Brill Italic Bold Italic','Brill Italic Medium Italic','Brill Italic Semibold Italic', //'Brill Italic Italic', 'Klee Demibold','Klee Medium', 'Hiragino Sans CNS W3','Hiragino Sans CNS W6', 'Hiragino Sans TC W3','Hiragino Sans TC W6', 'Lantinghei SC Demibold','Lantinghei SC Extralight','Lantinghei SC Heavy', 'Lantinghei TC Demibold','Lantinghei TC Heavy', 'LingWai SC Medium','LingWai TC Medium', 'Publico Headline Black','Publico Headline Bold', //'Publico Headline Black Italic','Publico Headline Bold Italic','Publico Headline Italic', 'Publico Text Bold','Publico Text Semibold', //'Publico Text Bold Italic','Publico Text Italic','Publico Text Semibold Italic', 'Toppan Bunkyu Midashi Gothic Extrabold','Toppan Bunkyu Midashi Mincho Extrabold', 'Weibei SC Bold','Weibei TC Bold', 'Xingkai SC Bold','Xingkai SC Light', 'Xingkai TC Bold','Xingkai TC Light', 'YuGothic Bold','YuGothic Medium', 'YuKyokasho Bold','YuKyokasho Medium', 'YuKyokasho Yoko Bold','YuKyokasho Yoko Medium', 'YuMincho Demibold','YuMincho Extrabold','YuMincho Medium', // SyntaxError: An invalid or illegal string was specified // 'YuMincho +36p Kana Demibold','YuMincho +36p Kana Extrabold','YuMincho +36p Kana Medium', ], windows: [ 'Arial Nova','Georgia Pro','Gill Sans Nova','Ink Free','Neue Haas Grotesk Text Pro','Rockwell Nova', 'Segoe Fluent Icons','Segoe UI Variable Display','Segoe UI Variable Small','Segoe UI Variable Text', 'Simplified Arabic Fixed','Verdana Pro', 'HoloLens MDL2 Assets', // removed from base FF136+ 1942883 // 1957317: Noto Sans CJK HK/JP/KR/SC/TC and Noto Serif CJK HK/JP/KR/SC/TC // ^ not added: windows only installs region-specific e.g. Noto Sans TC, not language-specific e.g. Noto Sans CJK TC // other 'Sans Serif Collection', // win11 only: 1858357 'MingLiU_MSCS','細明體_MSCS','MingLiU_MSCS-ExtB','細明體_MSCS-ExtB', // win11 only? 2016678 'Cascadia Code','Cascadia Mono', // MS downloads 11 'Arial Unicode MS','MS Reference Specialty','MS Outlook','Gill Sans','Gill Sans MT', // MS products 'OpenSymbol', // openoffice 'Amiri', // libreoffice ], windowsfaces: [ 'Arial Nova Cond','Arial Nova Light', 'Georgia Pro Black','Georgia Pro Cond','Georgia Pro Light','Georgia Pro Semibold', 'Gill Sans Nova Cond','Gill Sans Nova Light', //'Neue Haas Grotesk Text Pro UltraThin','Neue Haas Grotesk Text Pro Light', 'Rockwell Nova Cond','Rockwell Nova Extra Bold','Rockwell Nova Light', 'Verdana Pro Black','Verdana Pro Light', // the above are all supplemental, so to properly test font face is not leaking // we need to add some non-weighted fonts: not much to work with :-( 'Ink Free','Sans Serif Collection', // MS products 'Arial Unicode MS','MS Reference Specialty','MS Outlook','Gill Sans','Gill Sans MT', ], windowsoffscreen: [ 'Arial Nova','Georgia Pro','Gill Sans Nova','Ink Free','Neue Haas Grotesk Text Pro','Rockwell Nova', 'Segoe Fluent Icons','Segoe UI Variable Display','Segoe UI Variable Small','Segoe UI Variable Text', 'Simplified Arabic Fixed','Verdana Pro', // win11 'Sans Serif Collection', // MS products 'Arial Unicode MS','MS Reference Specialty','MS Outlook','Gill Sans','Gill Sans MT', // MS downloads 'Cascadia Code','Cascadia Mono', // 11 ], }, // platform detection platform: { // gecko gecko: [ // note: '-apple-menu','-apple-status-bar' not detected in gecko '-apple-system', 'Dancing Script', // android fallback // 'Roboto' 'MS Shell Dlg \\32', ], // non-gecko: all expected fonts that wouldn't likely be found on other platforms android: ['Dancing Script'], mac: [ '-apple-menu','-apple-status-bar','-apple-system', // wekbit detects these 3, blink doesn't 'Apple Color Emoji','Apple Symbols','AppleGothic','AppleMyungjo', ], windows: [ 'MS Gothic','MS PGothic','MS UI Gothic','Microsoft JhengHei','Microsoft New Tai Lue', 'Microsoft PhagsPa','Microsoft Tai Le','Microsoft YaHei','Microsoft Yi Baiti', ], // combined non-gecko all: [], }, } function set_fntList() { let build = (gLoad || isFontSizesMore !== isFontSizesPrevious) if (build) { isFontSizesPrevious = isFontSizesMore fntData = { all: {}, faces: {base: [], baselang: [], fpp: [], full: [], unexpected: []}, family: { base: [], baselang: [], bundled: [], control: [], 'control_name': [], fpp: [], full: [], generic: [], 'generic_name': [], system: [], unexpected: [] }, offscreen: {base: [], baselang: [], fpp: [], full: [], unexpected: []}, } // fntString if (isBB || 'android' == isOS || 'linux' == isOS) { // isBB: all maxed: linux: 120/140 | windows 131/183 | mac 135/150 // nonBB: seems to work well so far fntString = '-' } else { if ('windows' == isOS) {fntString = 'MōΩ' // 158/190 = max } else {fntString = 'Mō-'} // mac: 441/464 = almost max | Mōá?- + tofu = 443 } fntString = fntString +'\uffff' // baseSize: add fallback for misconfigured/missing // fntPlatformFont: expected + isn't/can't be blocked // when used forces a single fallback font to compare to instead of trying up to // three (monospace, sans-serif, serif). No entropy is lost as lack of aliases or // FontSubtitutes (e.g. Tahoma) is expected - we _know_ they are there let baseSize = ['monospace','sans-serif','serif'] fntPlatformFont = undefined // reset if ('windows' == isOS) { if (!isFontSizesMore) { // blink doesn't allow MS Shell Dlg \\32 fntPlatformFont = isGecko ? 'MS Shell Dlg \\32' : 'Tahoma' } if (isBB) {fntPlatformFont = undefined} // force BB to detect all fonts for health baseSize = [ 'monospace, Consolas, Courier, \"Courier New\", \"Lucida Console\"', 'sans-serif, Arial', 'serif, Times, Roman' ] } else if ('mac' == isOS) { if (isGecko && !isFontSizesMore) {fntPlatformFont = '-apple-system'} baseSize = [ 'monospace, Menlo, \"Courier New\", Monaco', 'sans-serif', 'serif' ] } else if ('android' == isOS) { // Roboto is not guaranteed unless Android 9+ if (!isFontSizesMore) {fntPlatformFont = 'Dancing Script'} } // control: 1-pass or 3-pass | control_name: remove fallbacks e.g. 'serif, X' -> 'serif' fntData.family.control = fntPlatformFont == undefined ? baseSize : [fntPlatformFont] fntData.family.control.forEach(function(name) {fntData.family['control_name'].push(name.split(',')[0])}) // generic: expand baseSize // don't use isStyles as that will duplicate and complicate existing entries with fallbacks // note: 'fantasy' = not set in gecko (checked Feb 2026) see 536004#c2 but lets cover other engines baseSize = baseSize.concat(['cursive','fantasy','fangsong','math','system-ui']) baseSize = baseSize.concat(isSystemFont) if (fntPlatformFont !== undefined) {baseSize.push(fntPlatformFont)} fntData.family.generic = baseSize.sort() baseSize.forEach(function(name) {fntData.family['generic_name'].push(name.split(',')[0])}) // font-family if (isOS !== undefined) { fntFake = '--00'+ rnd_string() let array = [], aExtraTests = ['faces','offscreen'] if ('android' == isOS) { // notos fntMaster.android.notoboth.forEach(function(fnt) {array.push('Noto Sans '+ fnt, 'Noto Serif '+ fnt)}) fntMaster.android.notosans.forEach(function(fnt) {array.push('Noto Sans '+ fnt)}) fntMaster.android.notoserif.forEach(function(fnt) {array.push('Noto Serif '+ fnt)}) // +extras array = array.concat(fntMaster.system[isOS]) fntData.family.full = array fntData.family.full.push(fntFake) } else if (isBB) { // TB44461: Segoe MDL2 Assets if (140 == isVer) { fntMaster.allowlist.windows = fntMaster.allowlist.windows.filter(x => !['Segoe MDL2 Assets'].includes(x)) fntMaster.blocklist.windows.push('Segoe MDL2 Assets') } // desktop BB let aBundled = [] fntMaster.bundled.notoboth.forEach(function(fnt) {aBundled.push('Noto Sans '+ fnt, 'Noto Serif '+ fnt)}) fntMaster.bundled.notosans.forEach(function(fnt) {aBundled.push('Noto Sans '+ fnt)}) fntMaster.bundled.notoserif.forEach(function(fnt) {aBundled.push('Noto Serif '+ fnt)}) aBundled = aBundled.concat(fntMaster.bundled[isOS]) array = array.concat(aBundled) fntData.family.bundled = array fntData.family.system = fntMaster.allowlist[isOS] array = array.concat(fntMaster.allowlist[isOS]) fntData.family.base = array fntMaster.blocklist[isOS].push(fntFake) fntData.family.unexpected = fntMaster.blocklist[isOS] array = array.concat(fntMaster.blocklist[isOS]) fntData.family.full = array // faces.offscreen aExtraTests.forEach(function(item) { let key = isOS + item array = fntMaster.allowlist[key] if (undefined !== array) { let aUnexpected = fntMaster.blocklist[key] fntData[item].base = array.sort() fntData[item].unexpected = aUnexpected.sort() fntData[item].full = array.concat(aUnexpected).sort() } }) } else { // desktop FF array = fntMaster.base[isOS] fntData.family.base = array array = array.concat(fntMaster.baselang[isOS]) fntData.family.fpp = array // windows FPP (mac FPP = same as base) fntData.family.baselang = fntMaster.baselang[isOS] fntMaster.system[isOS].push(fntFake) array = array.concat(fntMaster.system[isOS]) fntData.family.unexpected = fntMaster.system[isOS] fntData.family.full = array // faces/offscreen aExtraTests.forEach(function(item) { let key = isOS + item, array = fntMaster.base[key] if (undefined !== array) { let aBaseLang = fntMaster.baselang[key] let aFPP = undefined == aBaseLang ? array : array.concat(aBaseLang) let aUnexpected = fntMaster.system[key] fntData[item].base = array.sort() if (undefined !== aBaseLang) { fntData[item].baselang = aBaseLang.sort() } if (undefined !== aBaseLang) {fntData[item].fpp = aFPP.sort()} fntData[item].unexpected = aUnexpected.sort() fntData[item].full = aFPP.concat(aUnexpected).sort() } }) } // -control from lists if (fntPlatformFont !== undefined) { let fntKeys = ['base','full','fpp','system','bundled'] fntKeys.forEach(function(key) { if (fntData.family[key] !== undefined) { let array = fntData.family[key] fntData.family[key] = array.filter(x => ![fntPlatformFont].includes(x)) } }) } // dupes if (gLoad) { let aCheck = fntData.family.full aCheck = dedupeArray(aCheck) if (aCheck.length !== fntData.family.full.length) { log_alert(12, 'set_fntList', 'dupes in '+ isOS, isScope, true) // persist since we only do this once fntData.family.full = aCheck } } // sort fntData.family.bundled.sort() fntData.family.system.sort() fntData.family.unexpected.sort() fntData.family.base.sort() fntData.family.baselang.sort() fntData.family.fpp.sort() fntData.family.full.sort() // fntBtn let fntobj = {}, obj = fntData.family if (!isGecko || 'android' == isOS || !isBB && 'linux' == isOS) { fntobj = fntData.family.full } else if (isBB || 'windows' == isOS) { if (isBB) { fntobj = {'1. system': {count: obj.system.length, 'fonts': obj.system}, '2. bundled': {count: obj.bundled.length, 'fonts': obj.bundled}, '3. allowlist': {count: obj.base.length, 'fonts': obj.base}, } } else { fntobj = {'1. kBaseFonts': {count: obj.base.length, 'fonts': obj.base}, '2. kLangPackFonts': {count: obj.baselang.length, 'fonts': obj.baselang}, '3. FPP': {count: obj.fpp.length, 'fonts': obj.fpp}, } } fntobj['4. unexpected'] = {count: obj.unexpected.length, 'fonts': obj.unexpected} fntobj['5. tested'] = {count: obj.full.length, 'fonts': obj.full} } else { //mac fntobj = {'1. kBaseFonts': {count: obj.base.length, 'fonts': obj.base}, '2. unexpected': {count: obj.unexpected.length, 'fonts': obj.unexpected}, '3. tested': {count: obj.full.length, 'fonts': obj.full}, } } fntData.family['summary'] = fntobj fntBtn = addButton(12, 'fonts_'+ isOS, fntData.family.full.length +' fonts', 'btnc', 'lists') // faces/offscreen aExtraTests.forEach(function(item) { let obj = fntData[item] if (obj.full.length) { if (!isGecko) { fntobj = obj.full } else if (isBB) { fntobj = {'1. allowlist': {count: obj.base.length, 'fonts': obj.base}, '2. unexpected': {count: obj.unexpected.length, 'fonts': obj.unexpected}, '3. tested': {count: obj.full.length, 'fonts': obj.full} } } else if ('mac' == isOS) { fntobj = {'1. kBaseFonts': {count: obj.base.length, 'fonts': obj.base}, '2. unexpected': {count: obj.unexpected.length, 'fonts': obj.unexpected}, '3. tested': {count: obj.full.length, 'fonts': obj.full} } } else { fntobj = {'1. kBaseFonts': {count: obj.base.length, 'fonts': obj.base}, '2. kLangPackFonts': {count: obj.baselang.length, 'fonts': obj.baselang}, '3. FPP': {count: obj.fpp.length, 'fonts': obj.fpp}, '4. unexpected': {count: obj.unexpected.length, 'fonts': obj.unexpected}, '5. tested': {count: obj.full.length, 'fonts': obj.full} } } fntData[item]['summary'] = fntobj fntBtn = addButton(12, 'font_' + item +'_'+ isOS, fntData[item].full.length +' '+ item, 'btnc', 'lists') + fntBtn } }) // all: gecko only since non-gecko is an array and gecko is an object if (isGecko && fntData.faces.full.length) { let total for (const k of Object.keys(fntData.faces.summary)) { let array = [], aFace = fntData.faces.summary[k].fonts aFace.forEach(function(font){ // strip out regular and other text needed for font face if (' Regular' == font.slice(-8)) {font = font.slice(0,-8)} array.push(font) }) // dedupe array = dedupeArray(array) let newkey = k if (isBB) { newkey = ((k.slice(0,1)) * 1) + 2 newkey += k.slice(1) } array = array.concat(fntData.family.summary[newkey].fonts) try {array = array.concat(fntData.offscreen.summary[k].fonts)} catch {} array = dedupeArray(array) // remove unexpected if they're in the allow/expected lot if (k.includes('unexpected')) { let ignore = isBB ? '1. allowlist' : ('mac' == isOS ? '1. kBaseFonts' :'3. FPP') array = array.filter(x => !fntData.all[ignore].fonts.includes(x)) } else if (k.includes('tested')) { total = array.length } fntData.all[k] = {count: array.length, 'fonts': array.sort()} } fntBtn = addButton(12, 'total_fonts_'+ isOS, total +' total', 'btnc', 'lists') + fntBtn } } } // bail if (isOS == undefined) {return} // fnt*Btn data if (gRun || build) { addDetail('fonts_'+ isOS, fntData.family.summary, 'lists') addDetail('font_faces_'+ isOS, fntData.faces.summary, 'lists') addDetail('font_offscreen_'+ isOS, fntData.offscreen.summary, 'lists') addDetail('total_fonts_'+ isOS, fntData.all, 'lists') } } function set_fntList_mini() { // populate fntMaster.platform.all for non-gecko try { let aTemp = fntMaster.platform.android aTemp = aTemp.concat(fntMaster.platform.mac) aTemp = aTemp.concat(fntMaster.platform.windows) fntMaster.platform.all = aTemp.sort() } catch(e) {} } function get_document_fonts(METRIC) { fntDocEnabled = false // reset let value, data, notation = default_red, fntTest = '\"test font name\"' try { if (runSE) {foo++} // dedicated div, hardcoded style let font = getComputedStyle(dom.tzpDocFont).getPropertyValue('font-family'), fontnoquotes = font.slice(0, fntTest.length - 2) // ext may strip quotes marks fntDocEnabled = (font == fntTest || fontnoquotes == fntTest ? true : false) // test setting it: catches e.g. chameleon dom.tzpDiv.style.fontFamily = fntTest let font2 = getComputedStyle(dom.tzpDiv).getPropertyValue('font-family') // tidy value = (fntDocEnabled ? zE : zD) +' | '+ font + (font !== font2 ? ' | '+ font2 : '') // notate: only default if exact match if ('enabled | \"test font name\"' == value) {notation = default_green} } catch(e) { value = e; data = zErrLog } addBoth(12, METRIC, value,'', notation, data) return } function get_font_notation(METRIC, data) { if (!isGecko) {return ''} let badnotation = isBB ? bb_red : rfp_red let goodnotation = isBB ? bb_green : rfp_green let isSizes = 'font_sizes' == METRIC let obj = isSizes ? fntData.family : ('font_faces' == METRIC ? fntData.faces : fntData.offscreen) let notation = goodnotation let aNotInBase = data, aMissing = [], aMissingSystem = [] aNotInBase = aNotInBase.filter(x => !obj.base.includes(x)) if (isBB) { aMissing = isSizes ? obj.bundled : obj.base aMissing = aMissing.filter(x => !data.includes(x)) if (isSizes && obj.system.length) { aMissingSystem = obj.system aMissingSystem = aMissingSystem.filter(x => !data.includes(x)) } } let count = aNotInBase.length + aMissing.length + aMissingSystem.length if (count > 0) { let tmpName = (isSizes ? 'font_names' : METRIC) +'_health', tmpobj = {} let suffix = isSizes ? '_bundled' : '' if (aMissing.length) {tmpobj['missing' + suffix] = aMissing} if (aMissingSystem.length) {tmpobj['missing_system'] = aMissingSystem} if (aNotInBase.length) { if (isBB) { tmpobj['unexpected'] = aNotInBase } else { // FF: break into FPP vs non FPP tmpobj['unexpected'] = {} let aLangPack = aNotInBase.filter(x => obj.baselang.includes(x)) let aOther = aNotInBase.filter(x => !aLangPack.includes(x)) if (aLangPack.length) {tmpobj.unexpected['kLangPackFonts'] = aLangPack} if (aOther.length) {tmpobj.unexpected['other'] = aOther} } } addDetail(tmpName, tmpobj) let brand = isTB ? (isMB ? 'MB' : 'TB') : 'RFP' notation = addButton('bad', tmpName, "<span class='health'>"+ cross + '</span> '+ count +' '+ brand) // BB doesn't have baselang but check FPP fallback regardless if (isFPPFallback && obj.baselang.length) { // FFP if all unexpected are in baselang then we're fpp_green let aNotInBaseLang = aNotInBase.filter(x => !obj.baselang.includes(x)) if (aNotInBaseLang.length == 0) {notation = fpp_green} } } return notation } function get_fonts_base(METRICB, selected) { // selected can be: 'unknown', 'n/a' or any of the domrect or perspective or pixel // if n/a: try to calculate selected: same logic as font_sizes // in order, exclude lies + errors, limit to domrect + perspective or pixel if (selected == zNA) { let oDomList = {0: 'domrectbounding', 1: 'domrectclient', 2: 'domrectboundingrange', 3: 'domrectclientrange'} let order = [ 'domrectbounding','domrectboundingrange','domrectclient','domrectclientrange','perspective','pixel' ] if (isSmart) { for (const k of Object.keys(oDomList)) { if (!aDomRect[k]) {order = order.filter(x => ![oDomList[k]].includes(x))} // remove from list } } for (let i=0; i < order.length; i++) { let value = order[i] if (!fntBaseInvalid.hasOwnProperty(order[i])) {selected = value; break} } } let isSelected = selected !== zNA && selected !== 'unknown' // if we have fntBaseMin data _and_ nothing is invalid, output one of each method group let useMin = fntBaseMin.length > 0 && Object.keys(fntBaseInvalid).length == 0 // rebuild base fonts sizes: fntBase is already ordered: do first so hashes are correct // for each base combine w + h, replace with fntBaseInvalid errors (but not lies) // note: reported (non FP data) is grouped with all method data (and extensions can cause mayhem) // but select (a FP metric) should be grouped with only the method used, so we build them seperately let reportedTemp = {}, reportedHash = {}, reportedBase = {} let selectTemp = {}, selectHash = {}, selectBase = {} for (const base in fntBase) { // for each base e.g. serif reportedTemp[base] = {} for (const m of Object.keys(fntBase[base])) { // for each method e.g. domrectbounding if ('Width' == m.slice(-5)) { // for each pair let method = m.slice(0,-5), value if(zLIE !== fntBaseInvalid[method]) {value = fntBaseInvalid[method]} if (undefined == value) {value = [fntBase[base][m], fntBase[base][method +'Height']]} if (useMin && fntBaseMin.includes(method) || !useMin) { reportedTemp[base][method] = value if (isSelected && method == selected) {selectTemp[base] = value} } } } } // group by hash for (const base in reportedTemp) { // reported and selected have the same object keys // reported let tmphash = mini(reportedTemp[base]) if (undefined == reportedHash[tmphash]) { reportedHash[tmphash] = {'bases': [base], 'data': reportedTemp[base]} } else { reportedHash[tmphash].bases.push(base) } // select if (isSelected) { tmphash = mini(selectTemp[base]) if (undefined == selectHash[tmphash]) { selectHash[tmphash] = {'bases': [base], 'data': selectTemp[base]} } else { selectHash[tmphash].bases.push(base) } } } // use base as keys | bases are already sorted since fntData.family.generic is too for (const h in reportedHash) { let newhash = mini(reportedHash[h].data) reportedBase[reportedHash[h].bases.join(' ')] = {'hash': newhash, 'metrics': reportedHash[h].data} } if (isSelected) { for (const h in selectHash) { let newhash = mini(selectHash[h].data) selectBase[selectHash[h].bases.join(' ')] = selectHash[h].data } } //* reported //console.log('reportedTemp', reportedTemp) //console.log('reportedHash', reportedHash) //console.log('reportedBase', reportedBase) //*/ //* select //console.log('selectTemp', selectTemp) //console.log('selectHash', selectHash) //console.log('selectBase', selectBase) //*/ // display all that hard work!! // unless we had an error which means we never end up here, we will have fntBase data let btnAll = addButton(12, METRICB +'_reported', Object.keys(reportedBase).length +'/'+ fntData.family.generic_name.length) addDetail(METRICB +'_reported', reportedBase) addDisplay(12, METRICB+ '_reported', mini(reportedBase), btnAll) // add selected/unknown/n/a if (isSelected) { let newobj = {} newobj[selected] = selectBase let hash = mini(newobj) let btn = addButton(12, METRICB, Object.keys(selectBase).length +'/'+ fntData.family.generic_name.length) addBoth(12, METRICB, hash, btn,'', newobj) } else { addBoth(12, METRICB, selected) } } const get_fonts_faces = (METRIC, METRICD, aFonts) => new Promise(resolve => { // testing non regular fonts + font face leaks (i.e not just light/black etc) // it is problematic to test weighted fonts because you don't know // if it's synthesized, a variable font, or an actual font(name) // blocking document fonts does not affect this test let t0 = nowFn() // main test we don't pass an array of font names otherwise it's a test let isMain = undefined == aFonts let data ='', btn='', notation ='' function exit(value, btn, notation, data) { if (isMain) { addBoth(12, METRIC, value, btn, notation, data) fntHealth.push(notation) log_perf(12, METRIC, t0) } return resolve(data) } let typeCheck = typeFn(window.FontFace) if ('undefined' == typeCheck) {exit(typeCheck); return} // start with a letter or it throws "SyntaxError: An invalid or illegal string was specified" let fntFaceFake = 'a'+ rnd_string() async function testLocalFontFamily(font) { try { const fontFace = new FontFace(font, `local("${font}")`) await fontFace.load() return fntFaceFake } catch(e) { return e+'' } } function getLocalFontFamily(font) { return new FontFace(font, `local("${font}")`) .load() .then((font) => font.family) //.then((font) => font.unicodeRange +': '+ font.family) .catch(() => null) } function loadFonts(fonts) { return Promise.all(fonts.map(getLocalFontFamily)) .then(list => list.filter(font => font !== null)) } Promise.all([ testLocalFontFamily(fntFaceFake), ]).then(function(res){ let value ='' let fntList = isMain ? fntData.faces.full : aFonts let isNotate = fntList.length > 0 // only notate if we're testing it let badnotation = !isNotate ? '' : isBB ? bb_red : rfp_red let goodnotation = !isNotate ? '' : isBB ? bb_green : rfp_green try { let test = res[0] if (fntFaceFake == test) {throw zErrInvalid +'fake font detected' } else if ('NetworkError: A network error occurred.' !== test) {throw test } else if (!isNotate) { exit(zNA, btn, badnotation, data) } else { loadFonts(fntList).then(function(results){ if (results.length) { data = [] // some engines record quotes: strip them out results.forEach(function(item){ item = item.replaceAll('"',''); data.push(item) }) value = mini(results) btn = addButton(12, METRIC, results.length) if (isMain && fntList.length) { notation = get_font_notation(METRIC, data) // enumerate fonts across all font tests sDetail[isScope][METRICD] = sDetail[isScope][METRICD].concat(data) } } else { // ToDo: once we allow fontFace in BB this will always be badnotation notation = isBB ? goodnotation : badnotation value = 'none' if (!isMain) {data = 'none'} } exit(value, btn, notation, data) }) } } catch(e) { if (isMain) {exit(log_error(12, METRIC, e), btn, notation, zErr)} else {exit(zErr, btn, notation, zErr)} } }) }) function get_fonts_offscreen(METRIC, METRICD) { // test RFP/FPP do not leak // note: document fonts does not affect this test let t0 = nowFn() let fntList = fntData.offscreen.full if (0 == fntList.length) { addBoth(12, METRIC, zNA) return } // do a single pass // use fntPlatformFont if possible otherwise use monospace (with fallbacks if possible) let fntOffscreen = 'monospace' if (undefined !== fntPlatformFont) { fntOffscreen = fntPlatformFont } else if ('windows' == isOS) { fntOffscreen = 'monospace, Consolas, Courier, \"Courier New\", \"Lucida Console\"' } else if ('mac' == isOS) { fntOffscreen = 'monospace, Menlo, \"Courier New\", Monaco' } let value = '', data ='', btn='', notation = rfp_red let badnotation = isBB ? bb_red : rfp_red let goodnotation = isBB ? bb_green : rfp_green try { // set canvas let canvas = new OffscreenCanvas(0,0) let ctx = canvas.getContext('2d') // get base ctx.font = 'normal normal normal 512px '+ fntOffscreen let base = ctx.measureText(fntString).width // check base if (runST) {base = undefined} let typeCheck = typeFn(base) if ('number' !== typeCheck) {throw zErrType + typeCheck} // check fake font ctx.font = 'normal normal normal 512px '+ fntFake +', '+ fntOffscreen let fake = ctx.measureText(fntString).width if (runSI) {fake = base} if (fake !== base) {throw zErrInvalid +'fake font detected'} // loop font list data = [] fntList.forEach(function(font){ ctx.font = 'normal normal normal 512px '+ font +', '+ fntOffscreen if (ctx.measureText(fntString).width !== base) {data.push(font)} }) if (data.length) { value = mini(data) btn = addButton(12, METRIC, data.length) if (fntData.offscreen.base.length) {notation = get_font_notation(METRIC, data)} // enumerate fonts across all font tests sDetail[isScope][METRICD] = sDetail[isScope][METRICD].concat(data) } else { // ToDo: once we allow fontFace in BB this will always be badnotation notation = isBB ? goodnotation : badnotation value = 'none' } } catch(e) { value = e; data = zErrLog // tmp until we enable offscreen canvas in BB if (isBB && 'ReferenceError: OffscreenCanvas is not defined' == value) {notation = ''} } fntHealth.push(notation) addBoth(12, METRIC, value, btn, notation, data) log_perf(12, METRIC, t0) return } const get_fonts_size = (isMain = true, METRIC = 'font_sizes') => new Promise(resolve => { /* getDimensions code based on https://github.com/abrahamjuliot/creepjs */ // reset fntBaseInvalid = {} fntBaseMin = [] const id = 'element-fp' // note: element-fp has a transform: this only affects domrect try { if (runSE) {foo++} const doc = document // or iframe.contentWindow.document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) dom[id].innerHTML = ` <style> #${id}-detector { --font: ''; position: absolute !important; left: -9999px!important; font-size: ` + fntSize + ` !important; font-style: normal !important; font-weight: normal !important; font-stretch: normal !important; letter-spacing: normal !important; line-break: auto !important; line-height: normal !important; text-transform: none !important; text-align: left !important; text-decoration: none !important; text-shadow: none !important; white-space: normal !important; word-break: normal !important; word-spacing: normal !important; /* in order to test scrollWidth, clientWidth, etc. */ padding: 0 !important; margin: 0 !important; /* in order to test inlineSize and blockSize */ writing-mode: horizontal-tb !important; /* for transform and perspective */ transform-origin: unset !important; perspective-origin: unset !important; } #${id}-detector::after { font-family: var(--font); content: '`+ fntString +`'; } </style> <span id="${id}-detector"></span>` const span = doc.getElementById(`${id}-detector`) const pixelsToNumber = pixels => +pixels.replace('px','') const originPixelsToNumber = pixels => 2*pixels.replace('px', '') const style = getComputedStyle(span) const range = document.createRange() range.selectNode(span) // set parameters let fntGeneric = [], fntTest = [], fntControl = [], fntControlObj = {}, oTests = {}, aTests = [] const aSkipCheck = ['domrectbounding', 'domrectclient','domrectboundingrange','domrectclientrange','client','offset'] let oSkip = {}, isSkip = false for (let i=0; i < aSkipCheck.length; i++) {oSkip[i] = false} if (isMain) { fntData.family.control.forEach(function(item) { let key = item.split(',')[0] fntControl.push(key) fntControlObj[key] = item }) fntGeneric = fntData.family.generic fntTest = fntData.family.full // match display order so btn links = first of each hash oTests = { 'client': {}, 'offset': {}, 'scroll': {}, 'pixel': {}, 'pixelsize': {}, 'perspective': {}, 'transform': {}, 'domrectbounding': {}, 'domrectboundingrange': {}, 'domrectclient': {}, 'domrectclientrange': {}, } // if one dimension key throws e.g. a Reference Error then the whole lot thows // e.g. clientHeight: foo++, all three size metrics are errors: "font_sizes/_base/_methods": "ReferenceError: foo is not defined" // e.g. uBO's AOPR: *##+js(aopr, Range.prototype.getClientRects) // note:: we already catch TypeErrors in validity tests later let el = dom.tzpRect for (let i=0; i < aSkipCheck.length; i++) { let name = aSkipCheck[i] // Hmmm: these still create a complete meltdown instead of trapping individual errors // 1: *##+js(aopr, Element.prototype.getClientRects) // 2: !*##+js(aopr, Range.prototype.getBoundingClientRect) try { //foo++ let obj = {} if (0 == i) { //foo++ obj = el.getBoundingClientRect() } else if (1 == i) { obj = el.getClientRects()[0] // } else if (i < 4) { let range = document.createRange(); range.selectNode(el) if (2 == i) { obj = range.getBoundingClientRect() // } else { obj = range.getClientRects()[0] } } else if ('client' == name) { obj = {'height': el.clientHeight, 'width': el.clientWidth} } else if ('offset' == name) { obj = {'height': el.offsetHeight, 'width': el.offsetWidth} } } catch(e) { // if we susccessfully complete _something_ then we can add // errors and display for each skipped item later oSkip[i] = e+'' delete oTests[name] isSkip = true } } if (isSkip) {'skipped', console.log(oSkip)} } else { fntControl = ['monospace', "sans-serif", "serif"] fntControlObj = { "monospace": 'monospace, Consolas, Courier, "Courier New", "Lucida Console"', "sans-serif": 'sans-serif, Arial', "serif": 'serif, "Times New Roman\"', } fntGeneric = fntControl fntTest = ['--00'+ rnd_string()] let src = isGecko ? 'gecko' : 'all' fntTest = fntTest.concat(fntMaster.platform[src]) oTests = {'perspective': {}} } let getDimensions = (span, style) => { const transform = style.transformOrigin.split(' ') const perspective = style.perspectiveOrigin.split(' ') const dimensions = { // keep sorted for font_sizes_base_reported clientHeight: oSkip[4] ? '' : span.clientHeight, clientWidth: oSkip[4] ? '' : span.clientWidth, domrectboundingHeight: oSkip[0] ? '' : span.getBoundingClientRect().height, domrectboundingWidth: oSkip[0] ? '' : span.getBoundingClientRect().width, domrectboundingrangeHeight: oSkip[1] ? '' : range.getBoundingClientRect().height, domrectboundingrangeWidth: oSkip[1] ? '' : range.getBoundingClientRect().width, domrectclientHeight: oSkip[2] ? '' : span.getClientRects()[0].height, domrectclientWidth: oSkip[2] ? '' : span.getClientRects()[0].width, domrectclientrangeHeight: oSkip[3] ? '' : range.getClientRects()[0].height, domrectclientrangeWidth: oSkip[3] ? '' : range.getClientRects()[0].width, offsetHeight: oSkip[5] ? '' : span.offsetHeight, offsetWidth: oSkip[5] ? '' : span.offsetWidth, perspectiveHeight: originPixelsToNumber(perspective[1]), perspectiveWidth: originPixelsToNumber(perspective[0]), pixelHeight: pixelsToNumber(style.height), pixelWidth: pixelsToNumber(style.width), pixelsizeHeight: pixelsToNumber(style.blockSize), pixelsizeWidth: pixelsToNumber(style.inlineSize), scrollHeight: span.scrollHeight, scrollWidth: span.scrollWidth, transformHeight: originPixelsToNumber(transform[1]), transformWidth: originPixelsToNumber(transform[0]), } return dimensions } // simulate errors: don't test isFontSizesMore not used in production if (runSF && isMain && !isFontSizesMore) { getDimensions = (span, style) => { const transform = style.transformOrigin.split(' ') const perspective = style.perspectiveOrigin.split(' ') const dimensions = { clientHeight: span.clientHeight, // same size: engineered below clientWidth: span.clientWidth, domrectboundingHeight: null, // TypeError: empty string x null domrectboundingWidth: '', domrectboundingrangeHeight: range.getBoundingClientRect().height, domrectboundingrangeWidth: range.getBoundingClientRect().width, domrectclientHeight: span.getClientRects()[0].height, // fake font detected: engineered below domrectclientWidth: span.getClientRects()[0].width, domrectclientrangeHeight: 100, // none domrectclientrangeWidth: 200, offsetHeight: NaN, // TypeError: NaN (same) offsetWidth: NaN, perspectiveHeight: undefined, // TypeError: Infinity x undefined (different) perspectiveWidth: Infinity, pixelHeight: pixelsToNumber(style.height), pixelWidth: pixelsToNumber(style.width), pixelsizeHeight: pixelsToNumber(style.blockSize), pixelsizeWidth: pixelsToNumber(style.inlineSize), scrollHeight: 0, // Invalid: width or height < 1 scrollWidth: 50, transformHeight: originPixelsToNumber(transform[1]) + ((Math.random() * 100) / 100), // all transformWidth: originPixelsToNumber(transform[0]), } return dimensions } } // base sizes fntBase = fntGeneric.reduce((acc, font) => { if (isSystemFont.includes(font)) { // not a family span.style.setProperty('--font', '') span.style.font = font } else { span.style.font ='' span.style.setProperty('--font', font) } const dimensions = getDimensions(span, style) // this line is where it errors acc[font.split(',')[0]] = dimensions // use only first name, i.e w/o fallback return acc }, {}) span.style.font ='' // reset // test validity for (const k of Object.keys(oTests)) { // assume we always have fntBase.monospace let wValue = fntBase.monospace[k +'Width'], wType = typeFn(wValue), hValue = fntBase.monospace[k +'Height'], hType = typeFn(hValue) try { if ('number' !== wType || 'number' !== hType) { throw zErrType + (wType == hType ? wType : wType +' x '+ hType) } else if (wValue < 1 || hValue < 1) {throw zErrInvalid + 'width or height < 1' } else if (wValue == hValue < 1) {throw zErrInvalid + 'width == height'} aTests.push(k) } catch(e) { fntBaseInvalid[k] = zErr oTests[k]['error'] = e+'' addDisplay(12, METRIC +'_'+ k, log_error(12, METRIC +'_'+ k, e)) } } // base only: after validity so we know what to use in lookup if (isMain) { if (!fntTest.length || false == fntDocEnabled) { removeElementFn(id) return resolve('baseonly') } } // measure if (aTests.length) { let intDetected = 0, intDetectedMax = aTests.length fntTest.forEach(font => { intDetected = 0 // reset per font for (const basefont of fntControl) { intDetected = 0 // reset per control span.style.setProperty('--font', "'"+ font +"', "+ fntControlObj[basefont]) const style = getComputedStyle(span) const dimensions = getDimensions(span, style) aTests.forEach(function(method) { let wName = method +'Width', hName = method +'Height' if (dimensions[wName] != fntBase[basefont][wName] || dimensions[hName] != fntBase[basefont][hName]) { if (isFontSizesMore) { // every basefont result if (undefined == oTests[method][font]) {oTests[method][font] = {}} oTests[method][font][basefont] = [dimensions[wName], dimensions[hName]] } else { // always one result per font oTests[method][font] = [dimensions[wName], dimensions[hName]] } intDetected++ } }) if (intDetected == intDetectedMax && !isFontSizesMore) {break} } }) } // exit isOS check if (!isMain) { removeElementFn(id) return resolve(oTests['perspective']) } // sim fake font + same sizes if (runSF && !isFontSizesMore) { oTests['domrectclient'][fntFake] = [700, 800] for (const k of Object.keys(oTests['client'])) {oTests['client'][k] = [700, 800]} } // catch more errors for (const k of Object.keys(oTests)) { let obj = oTests[k], objcount = Object.keys(obj).length try { if (0 == objcount) {throw zErrInvalid +'none' } else if (objcount == fntData.family.full.length) {throw zErrInvalid +'all' } else if (obj.hasOwnProperty(fntFake)) {throw zErrInvalid +'fake font detected'} } catch(e) { // we don't have to emmpty it for (const prop in obj) {if (obj.hasOwnProperty(prop)) {delete obj[prop]}} fntBaseInvalid[k] = zErr oTests[k]['error'] = e+'' addDisplay(12, METRIC +'_'+ k, log_error(12, METRIC +'_'+ k, e)) } } // add domrect lies if not already an error if (isSmart) { // in order of aDomRect let domrectnames = ['domrectbounding','domrectclient','domrectboundingrange','domrectclientrange'] for (let i=0; i < domrectnames.length; i++) { let name = domrectnames[i] if (!aDomRect[i] && undefined == fntBaseInvalid[name]) {fntBaseInvalid[name] = zLIE} } } // if we got this far we can add oSkip if (isSkip) { for (const k of Object.keys(oSkip)) { let skipErr = oSkip[k], m = aSkipCheck[k] if ('string' === typeof skipErr) {addDisplay(12, METRIC +'_'+ m, log_error(12, METRIC +'_'+ m, skipErr))} } } removeElementFn(id) return resolve(oTests) } catch(e) { removeElementFn(id) if (isMain) { log_error(12, METRIC, e) log_error(12, METRIC +'_methods', e) log_error(12, METRIC +'_base', e) } return resolve(zErr) } }) function get_fonts(METRIC, METRICD) { /* - only notate font_names == not a metric but is picked up health - sizes we record all errors + lies per method. This is all we need for method results/entropy - sizes is either something or unknown: so never notate or lies - sizes_base + sizes_methods: never notate or lies: it is simply a reflection of what happened in sizes */ let t0 = nowFn() const METRICM = METRIC +'_methods' const METRICB = METRIC +'_base' const METRICN = 'font_names' let badnotation = isBB ? bb_red : rfp_red let goodnotation = isBB ? bb_green : rfp_green // functions function exit(value) { addBoth(12, METRIC, value) addBoth(12, METRICM, value) add_font_names(value) if (value == zNA) { get_fonts_base(METRICB, value) } else { addBoth(12, METRICB, value) } log_perf(12, METRIC, t0) return } function add_font_names(value) { // fontnames: always notate for health // display only: so always add a lookup sDetail[isScope].lookup[METRICN] = value addDisplay(12, METRICN, value,'', badnotation) fntHealth.push(badnotation) } get_fonts_size().then(res => { //console.log("res", res) // quick exits let typeCheck = typeFn(res) if ('string' === typeCheck) {exit(('baseonly' == res ? zNA : zErr)); return} if ('object' !== typeCheck) {log_error(12, METRIC, zErrType + typeCheck); exit(zErr); return} // organize oData: note: everything is already sorted let oData = {}, oValid = {} for (let name in res) { let data = res[name] if (!data.hasOwnProperty('error')) { // group by hash let hash = mini(data) oValid[name] = hash if (oData[hash] == undefined) {oData[hash] = {'names': [name], 'data': data} } else {oData[hash].names.push(name)} } } // per hash: do stuff: font names, same size, handle isFontSizesMore for (const h of Object.keys(oData)) { oData[h].datacount = Object.keys(oData[h].data).length oData[h].datafonts = [] let oTmpSize = {}, setSize = new Set(), oGroups = {} for (const f of Object.keys(oData[h].data)) { oData[h].datafonts.push(f) // only do size buckets if not isFontSizesMore if (!isFontSizesMore) { let sizekey = oData[h].data[f].join('x') if (undefined == oTmpSize[sizekey]) {oTmpSize[sizekey] = [f], setSize.add(sizekey) } else {oTmpSize[sizekey].push(f)} } } // use size buckets to detect more garbage if (isFontSizesMore || setSize.size > 1) { oData[h].sizedata = [] // for detailed items oData[h].sizecount = setSize.size for (const k of Object.keys(oTmpSize)) { let tmpFonts = oTmpSize[k] let tmpSize = oData[h].data[tmpFonts[0]] oData[h].sizedata.push([tmpFonts, tmpSize]) } } else { (oData[h].names).forEach(function(name) { let lookup = oData[h].data[oData[h].datafonts[0]] let error = zErrInvalid +'same size ['+ lookup.join(' x ') +']' fntBaseInvalid[name] = zErr addDisplay(12, METRIC +'_'+ name, log_error(12, METRIC +'_'+ name, error)) }) delete oData[h] } } // sync fntBaseInvalid and oValid for (name in fntBaseInvalid) {delete oValid[name]} // fallback: first valid domrect in order of display as that gets the btn // fntBaseInvalid: is errors, plus lies if not an error and isSmart let selected let aDomList = ['domrectbounding','domrectboundingrange','domrectclient','domrectclientrange'] for (let i=0; i < aDomList.length; i++) { let domname = aDomList[i] if (undefined !== oValid[domname]) {selected = domname; break} } // font_size_methods: this gives us any tampering entropy // not to be confused with errors/lies which are already recorded let oMethods = {}, oIndex = {}, counter = 0 let aNames = [ // sorted by expected group then name 'client','offset','scroll','pixel','pixelsize','perspective','transform', 'domrectbounding','domrectboundingrange','domrectclient','domrectclientrange', ] aNames.forEach(function(k) { if (undefined !== oValid[k]) { let indexKey = oValid[k] // the method hash if (oIndex[indexKey] == undefined) { oIndex[indexKey] = (counter+'').padStart(2,'0'); counter++ } let mKey = oIndex[indexKey] if (oMethods[mKey] == undefined) { oMethods[mKey] = [k] fntBaseMin.push(k) // first of each } else { oMethods[mKey].push(k) } } }) let mHash = 'unknown', mBtn ='', mData ='' if (Object.keys(oMethods).length) { mHash = mini(oMethods); mBtn = addButton(12, METRICM); mData = oMethods } addBoth(12, METRICM, mHash, mBtn, '', mData) // fallbacks: matching valid *Number pairs if (selected == undefined) { let items = [['perspective', 'transform'], ['pixel', 'pixelsize']] for (let i=0; i < items.length; i++) { let ctrlName = items[i][0], ctrlHash = oValid[ctrlName] let testName = items[i][1], testHash = oValid[testName] if (ctrlHash !== undefined && ctrlHash == testHash) { selected = ctrlName; break // we have a valid *Number pair } } } //console.log('oData', oData) //console.log('oValid', oValid) //console.log('fntBaseInvalid', fntBaseInvalid) // no more fallbacks // output oData = not-errors for (const k of Object.keys(oData)) { let aList = oData[k].names for (let i=0; i < aList.length; i++) { let method = aList[i] let fntmethod = METRIC +'_'+ method // style + record lies to be consistent let isLies = false, btn ='' if ('domrect' == method.slice(0,7)) { isLies = fntBaseInvalid.hasOwnProperty(method) if (isLies) { // we don't need to record domrect lie data: just that its a lie log_known(12, fntmethod, zLIE) } } //add btn to first of each hash if (i == 0) { addDetail(fntmethod, oData[k].data) btn = addButton(12, fntmethod, oData[k].datacount) if (!isFontSizesMore) { addDetail(fntmethod +'_grouped', oData[k].sizedata) btn += addButton(12, fntmethod +'_grouped', oData[k].sizecount) } } addDisplay(12, fntmethod, k, btn,'', isLies) // FP data: not lies if (method == selected) { let notation ='' if (fntData.family.base.length) {notation = get_font_notation(METRIC, oData[k].datafonts)} // names let btn = addButton(12, METRICN, oData[k].datacount) sDetail[isScope][METRICN] = oData[k].datafonts addDisplay(12, METRICN, mini(oData[k].datafonts), btn, notation) fntHealth.push(notation) // data btn = addButton(12, METRIC, oData[k].datacount) if (!isFontSizesMore) { addDetail(METRIC +'_grouped', oData[k].sizedata) btn += addButton(12, METRIC +'_grouped', oData[k].sizecount) } addBoth(12, METRIC, k, btn,'', oData[k].data) // enumerate fonts across all font tests sDetail[isScope][METRICD] = sDetail[isScope][METRICD].concat(oData[k].datafonts) } } } // nothing if (!Object.keys(oData).length || selected == undefined) { addBoth(12, METRIC, 'unknown') add_font_names('unknown') get_fonts_base(METRICB, 'unknown') } else { get_fonts_base(METRICB, selected) } log_perf(12, METRIC, t0) return }) } function get_fonts_max(METRIC, isLies) { let t0 = nowFn() let value = zNA, data ='', btn='' let el = dom.tzpFontMax try { data = {} let range, method isStylesAll.forEach(function(stylename) { el.innerHTML = '<span class="'+ stylename +'" style="font-size: 20000px">.</span>' let target = el.children[0] method = measureFn(target, METRIC) if (undefined !== method.error) {throw method.errorstring} value = method.height if (runST) {value += ''} let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrInvalid + 'got '+ typeCheck} data[stylename] = value }) value = mini(data); btn = addButton(12, METRIC) } catch(e) { value = e; data = zErrLog } el.innerHTML ='' addBoth(12, METRIC, value, btn,'', data, isLies) log_perf(12, METRIC, t0) return } function get_formats() { // FF105+: layout.css.font-tech.enabled const oList = { 'font-format': ['collection','embeddedopentype','opentype','svg','truetype','woff','woff2'], 'font-tech': ['color-CBDT','color-COLRv0','color-COLRv1','color-SVG','color-sbix', 'features-aat','features-graphite','features-opentype','incremental','palettes','variations'] } for (const k of Object.keys(oList)) { let list = oList[k] const METRIC = k let hash, btn ='', data = [] try { if (runSE) {foo++} list.forEach(function(item) {if (CSS.supports(k +'('+ item + ')')) {data.push(item)}}) if (data.length) { hash = mini(data); btn = addButton(12, METRIC, data.length) } else { hash = zNA; data ='' } } catch(e) { hash = e; data = zErrLog } addBoth(12, METRIC, hash, btn,'', data) } return } function get_glyphs(METRIC, isLies) { /* NOTES FF131+ nightly: 1900175 + 1403931 ride the train - Enable USER_RESTRICTED for content processes on Nightly - security.sandbox.content.level > 7 - this affected (FF win11 at least) clientrect - 0x3095 + 0x532D (2 CJK chars) - almost always both in every style except cursive never affected - only changed in http(s), file:// not affected - so reminder that generally we should always be using https for final testing/analysis */ let t0 = nowFn() /* Notes: math added FF145+ nightly and FF149+ use isStyles currently = ['cursive','math','monospace','sans-serif','serif','system-ui'] unique sizes: win11 all system fonts FF sans-serif = 34 + cursive = 66 + serif = 84 + system-ui = 102 + monospace = 112 + fantasy = 115 - all the same: 'emoji','ui-monospace','ui-rounded','ui-sans-serif','ui-serif' - ui-* not added to gecko yet - emoji - we're not testing any emojis here - do not increase unique sizes - 'fangsong': does not add to unique sizes, we would need a different set of code points - 'fantasy': only added 3 more sizes */ const id = 'element-fp' let hash, btn ='', data = {}, strSizes ='' try { if (runSE) {foo++} const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) div.innerHTML = '<span id="glyphs-span" style="font-size: 22000px;"><span id="glyphs-slot"></span></span>' const span = dom['glyphs-span'], slot = dom['glyphs-slot'] let oData = {}, tmpobj = {}, newobj = {}, setSize = new Set() let methodW, methodH, width, height let methoddiv, methodspan, rangeH, rangeW // current method isStyles.forEach(function(stylename) { slot.style.fontFamily = stylename oData[stylename] = {} let isFirst = stylename == isStyles[0] fntCodes.forEach(function(code) { let codeString = String.fromCodePoint(code) slot.textContent = codeString // always get span width, div height /* use a dedicated function methodW = measureFn(div, METRIC) methodH = measureFn(span, METRIC) width = methodW.width, height = methodH.height // only typecheck once: first char on first style if (code == fntCodes[0] && isFirst) { if (undefined !== methodW.error) {throw methodW.errorstring} if (undefined !== methodH.error) {throw methodH.errorstring} if (runST) {width = NaN, height = [1]} let wType = typeFn(width), hType = typeFn(height) if ('number' !== wType || 'number' !== hType) { throw zErrType + (wType == hType ? wType : wType +' x '+ hType) } } //*/ // current method if (isDomRect > 1) { rangeH = document.createRange() rangeH.selectNode(div) rangeW = document.createRange() rangeW.selectNode(span) } if (isDomRect < 1) { // get a result regardless methoddiv = div.getBoundingClientRect() methodspan = span.getBoundingClientRect() } else if (isDomRect == 1) { methoddiv = div.getClientRects()[0] methodspan = span.getClientRects()[0] } else if (isDomRect == 2) { methoddiv = rangeH.getBoundingClientRect() methodspan = rangeW.getBoundingClientRect() } else if (isDomRect > 2) { methoddiv = rangeH.getClientRects()[0] methodspan = rangeW.getClientRects()[0] } width = methodspan.width, height = methoddiv.height // only typecheck once: first char on first style if (code == fntCodes[0] && isFirst) { if (runST) {width = NaN, height = [1]} let wType = typeFn(width), hType = typeFn(height) if ('number' !== wType || 'number' !== hType) { throw zErrType + (wType == hType ? wType : wType +' x '+ hType) } } oData[stylename][code] = [width, height] setSize.add(width+'x'+height) }) }) for (const k of Object.keys(oData)) { let hash = mini(oData[k]) if (tmpobj[hash] == undefined) {tmpobj[hash] = {'names': [k], 'data': oData[k]} } else {tmpobj[hash].names.push(k)} } for (const k of Object.keys(tmpobj)) {newobj[tmpobj[k].names.join(' ')] = tmpobj[k].data} for (const k of Object.keys(newobj).sort()) {data[k] = newobj[k]} hash = mini(data), strSizes = gRun ? setSize.size + ' unique sizes' : '' btn = addButton(12, METRIC) } catch(e) { hash = e; data = zErrLog } removeElementFn(id) addBoth(12, METRIC, hash, btn,'', data, isLies) log_perf(12, METRIC, t0,'', strSizes) return } function get_graphite(METRIC) { let hash, data ='', notation = isBB ? bb_red : '' try { if (!fntDocEnabled) {throw zErrInvalid + 'document fonts disabled'} // ToDo: handle when font face is blocked let el = dom.tzpGraphite, test = el.children[0].offsetWidth, control = el.children[1].offsetWidth if (runST) {test = NaN; control = NaN} let wType = typeFn(test), hType = typeFn(control) if ('number' !== wType || 'number' !== hType) {throw zErrType + wType +' | '+ hType} hash = (control == test ? zF : zS) if (isBB) {notation = hash == zS ? bb_standard : bb_safer} } catch(e) { hash = e; data = zErrLog } addBoth(12, METRIC, hash,'', notation, data) return } function get_script_defaults(METRIC) { // this is zoom resistant // except with "zoom text only" and you zoom from default // note: isStyles|All doesn't add anything const styles = ['monospace','sans-serif','serif'] const scripts = { arabic: 'ar', armenian: 'hy', bengali: 'bn', cyrillic: 'ru', devanagari: 'hi', ethiopic: 'gez', georgian: 'ka', greek: 'el', gujurati: 'gu', gurmukhi: 'pa', hebrew: 'he', japanese: 'ja', kannada: 'kn', khmer: 'km', korean: 'ko', latin: 'en', malayalam: 'ml', mathematics: 'x-math', odia: 'or', other: 'my', 'simplified chinese': 'zh-CN', sinhala: 'si', tamil: 'ta', telugu: 'te', thai: 'th', tibetan: 'bo', 'traditional chinese (hong kong)': 'zh-HK', 'traditional chinese (taiwan)': 'zh-TW', 'unified canadian syllabary': 'cr', } let hash = zNA, btn ='', data ='', notation = default_red //if (isGecko) { data = {} try { const el = dom.tzpScript // family typecheck let test = getComputedStyle(el).getPropertyValue('font-family') if (runST) {test =''} let typeCheck = typeFn(test) if ('string' !== typeCheck) {throw zErrType + 'font-family: '+ typeCheck} // size typecheck test = getComputedStyle(el).getPropertyValue('font-size').trim() if (runSI) {test = '16ppx'} let originalvalue = test typeCheck = typeFn(test) if ('string' !== typeCheck) {throw zErrType + typeCheck} if (test.slice(-2) !== 'px') {throw zErrInvalid + 'got '+ originalvalue} // missing px test = test.slice(0, -2) if (test.length > 0) {test = test * 1} if ('number' !== typeFn(test)) {throw zErrInvalid + 'got '+ originalvalue} // missing number // loop let tmpdata = {} el.style.fontSize ='' // reset size for (const k of Object.keys(scripts)) { let lang = scripts[k] el.style.fontFamily ='' // each lang reset fanily el.setAttribute('lang', lang) let font = getComputedStyle(el).getPropertyValue('font-family') let tmp = [font] styles.forEach(function(style) { el.style.fontFamily = style let size = getComputedStyle(el).getPropertyValue('font-size').slice(0,-2) tmp.push(size) }) let key = tmp.join('-') if (tmpdata[key] == undefined) {tmpdata[key] = [k]} else {tmpdata[key].push(k)} } let singleKey for (const k of Object.keys(tmpdata).sort()) {data[k] = tmpdata[k]; singleKey = k} // sort obj hash = mini(data); btn = addButton(12, METRIC) // notation if ('windows' == isOS && 'e5179dbb' == hash) {notation = default_green } else if ('linux' == isOS && 'a4253645' == hash) {notation = default_green } else if ('mac' == isOS && '884ca29d' == hash) {notation = default_green } else if ('android' == isOS && '632e080a' == hash) {notation = default_green } // single value if (1 === Object.keys(data).length) {hash = singleKey; btn = ''} } catch(e) { hash = e; data = zErrLog } //} addBoth(12, METRIC, hash, btn, notation, data) return } function get_system_fonts(METRIC) { // 1802957: FF109+: -moz no longer applied but keep for regression testing // add bogus '-default-font' to check they are falling back to actual default let oList = { 'fonts_moz': [ '-default-font','-moz-bullet-font','-moz-button','-moz-button-group','-moz-desktop','-moz-dialog','-moz-document', '-moz-field','-moz-info','-moz-list','-moz-message-bar','-moz-pull-down-menu','-moz-window','-moz-workspace', ], 'fonts_system': ['caption','icon','menu','message-box','small-caption','status-bar'] } let aProps = ['font-size','font-style','font-weight','font-family'] let hash, btn ='', data = {}, notation = 'moz_fonts' == METRIC ? default_red : rfp_red try { let tmpdata = {} let el = dom.tzpDiv // typecheck for (const j of aProps) { let test = getComputedStyle(el)[j] if (runST) {test =''} let typeCheck = typeFn(test) if ('string' !== typeCheck) {throw zErrType + j +': '+ typeCheck} } oList[METRIC].forEach(function(name){ let aKeys = [] el.style.font ='' // always clear in case a font is invalid/deprecated el.style.font = name for (const j of aProps) {aKeys.push(getComputedStyle(el)[j])} let key = aKeys.join(' ') if (tmpdata[key] == undefined) {tmpdata[key] = [name]} else {tmpdata[key].push(name)} }) let count = 0 for (const k of Object.keys(tmpdata).sort()) {data[k] = tmpdata[k]; count += tmpdata[k].length} hash = mini(data) // moz: defaults since at least 115 on win/linux: assume android/mac the same: i.e switch to generic font-families if ('fonts_moz' == METRIC) { if (isDesktop) { if ('fe778289' == hash) {notation = default_green} // 16px normal 400 serif } else if ('android' == isOS) { if ('7e76c987' == hash) {notation = default_green} // 16px normal 400 sans-serif } } else { // RFP FF128+ if ('windows' == isOS) { if ('a75e7a17' == hash) {notation = rfp_green} // 12px normal 400 sans-serif } else if ('mac' == isOS) { if ('0b6c0dbe' == hash) {notation = rfp_green} /* mac 11px normal 400 -apple-system: [message-box, status-bar], 11px normal 700 -apple-system: [small-caption], 12px normal 400 -apple-system: [icon], 13px normal 400 -apple-system: [caption, menu] */ } else if ('linux' == isOS) { if (isBB) { // BB14: due to font config aliases if ('ea0ea5d7' == hash) {notation = rfp_green} // 15px normal 400 Arimo } else { if ('48e3d1b4' == hash) {notation = rfp_green} // 15px normal 400 sans-serif } } else if ('android' == isOS) { if ('7e83ef35' == hash) {notation = rfp_green} // 12px normal 400 Roboto } } if (isSmart) {count = (count +'').padStart(2,' ')} // aesthetics: align the last three "font" health metrics btn = addButton(12, METRIC, Object.keys(data).length +'/'+ count) } catch(e) { hash = e; data = zErrLog } addBoth(12, METRIC, hash, btn, notation, data) return } function get_textmetrics(METRIC) { /* https://www.bamsoftware.com/talks/fc15-fontfp/fontfp.html#demo */ /* NOTES FF86+: 1676966: gfx.font_rendering.fallback.async - set chars directly in HTML to force fallback ASAP */ let t0 = nowFn() let oMetrics = { actualboundingbox: ['actualBoundingBoxAscent','actualBoundingBoxDescent','actualBoundingBoxLeft','actualBoundingBoxRight'], baseline: ['alphabeticBaseline','hangingBaseline','ideographicBaseline'], emheight: ['emHeightAscent','emHeightDescent'], fontboundingbox: ['fontBoundingBoxAscent','fontBoundingBoxDescent'], // width is mostly identical to glyphs domrect width (untransformed). A handful of font widths // differ by a tiny amount e.g. ±0.0001220703125 or -0.00006103515625. This is not going to add // much entropy if any, so drop for now //width: ['width'], } let oData = {}, aValid = [] try { if (runSE) {foo++} // check supported + type let aNonsense = ['', Infinity,' ', [], true, undefined, {1:2}, null, 'a'] let canvas = dom.tzpTextmetrics, ctx = canvas.getContext('2d') let tm = ctx.measureText('a') for (const k of Object.keys(oMetrics)) { oData[k] = {} let oSet = new Set() for (const j of oMetrics[k]) { let isSupported = runST ? Math.random() < 0.5 : TextMetrics.prototype.hasOwnProperty(j) if (isSupported) { let typeCheck = typeFn(tm[j]) if (runST && Math.random() < 0.5) { let x = aNonsense[Math.floor(Math.random() *10 )] typeCheck = typeFn(x) } if ('number' !== typeCheck) { isSupported = zErr let suffix = 'baseline' == k ? j.slice(0, j.length-8) : j.slice(k.length) if ('width' == k) { addBoth(12, METRIC +'_'+ k, log_error(12, METRIC +'_'+ k, zErrType + typeCheck)) } else { log_error(12, METRIC +'_'+ k +'_'+ suffix.toLowerCase(), zErrType + typeCheck) } } } oData[k][j] = isSupported oSet.add(isSupported) } if (1 == oSet.size && oSet.has(true)) {aValid.push(k) // test } else if (1 == oSet.size && oSet.has(false)) {addBoth(12, METRIC +'_'+ k, zNA) // not supported } else if ('width' == k) { // error: we already added width } else if (1 == oSet.size && oSet.has(zErr)) {addBoth(12, METRIC +'_'+ k, zErr +'s') // errors } else { let summary = [] for (const j of oMetrics[k]) {summary.push(oData[k][j])} addBoth(12, METRIC +'_'+ k, summary.join(', ')) // mixed } } if (0 == aValid.length) {return} let styles = isStyles // don't use 'none': this is default style + font per style for each language // and is already present in covering monospace/sans-serif/serif // fantasy vs sans-serif | fangsong vs serif both add very little oData = {} // clear aValid.forEach(function(k){ oData[k] = {} styles.forEach(function(s){oData[k][s] = {}}) }) let aSet = [], aList = ['actualboundingbox', 'width'] aList.forEach(function(m) {if (aValid.includes(m)) {aSet.push(m)}}) let bSet = [], bList = ['baseline', 'emheight', 'fontboundingbox'] bList.forEach(function(m) {if (aValid.includes(m)) {bSet.push(m)}}) styles.forEach(function(s) { // each style ctx.font = 'normal normal 22000px '+ s if (aSet.length) { fntCodes.forEach(function(code) { // each code let codeString = String.fromCodePoint(code) tm = ctx.measureText(codeString) // textmetrics aSet.forEach(function(k){ let data = [], props = oMetrics[k] props.forEach(function(p) {data.push(tm[p])}) oData[k][s][code] = data }) }) } if (bSet.length) { tm = ctx.measureText('a') bSet.forEach(function(k){ let data = [], props = oMetrics[k] props.forEach(function(p) {data.push(tm[p])}) oData[k][s] = data // we're only getting a single codepoint }) } }) //console.log(oData) aValid.forEach(function(k) { let tmpobj = {}, newobj = {}, data = {}, isLies = false // group hashes for (const s of Object.keys(oData[k])) { let hash = mini(oData[k][s]) if (undefined == tmpobj[hash]) {tmpobj[hash] = {'data': oData[k][s], 'names': [s]} } else {tmpobj[hash].names.push(s)} } for (const k of Object.keys(tmpobj)) {newobj[tmpobj[k].names.join(' ')] = tmpobj[k].data} // group by name for (const k of Object.keys(newobj).sort()) {data[k] = newobj[k]} // sort by name oMetrics[k].forEach(function(name){if (isProxyLie('TextMetrics.' + name)) {isLies = true}}) let hash = mini(data), btn = addButton(12, METRIC +'_'+ k) addBoth(12, METRIC +'_'+ k, hash, btn,'', data, isLies) }) } catch(e) { for (const k of Object.keys(oMetrics)) { addBoth(12, METRIC +'_'+ k, log_error(12, METRIC +'_'+ k, e)) } } log_perf(12, METRIC, t0) try {dom.tzpTextmetrics.height = 0} catch {} // hide the fixed canvas after use return } function get_widget_fonts(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input /* let aList = [ 'button','checkbox','color','date','datetime-local','email','file','hidden','image','month','number', 'password','radio','range','reset','search','select','submit','tel','text','textarea','time','url','week', ] */ let aProps = ['font-family','font-size'] let hash, btn='', data = {}, notation = rfp_red try { let tmpdata = {} let target = dom.tzpWidget for (let i=0; i < target.childElementCount; i++) { let el = target.children[i], name = '' !== el.id ? el.id.slice(3) : el.type let aKeys = [] for (const j of aProps) { let value = getComputedStyle(el)[j] // type check first item if (0 == i) { if (runST) {value =''} let typeCheck = typeFn(value) if ('string' !== typeCheck) {throw zErrType + j +': '+ typeCheck} } aKeys.push(value) } let key = aKeys.join(' ') if (tmpdata[key] == undefined) {tmpdata[key] = [name]} else {tmpdata[key].push(name)} } let count = 0 for (const k of Object.keys(tmpdata).sort()) {data[k] = tmpdata[k]; count += tmpdata[k].length} hash = mini(data) btn = addButton(12, METRIC, Object.keys(data).length +'/'+ count) // RFP FF128+ if ('windows' == isOS && '24717aa8' == hash) {notation = rfp_green /*monospace 13.3333px: [date, datetime-local, time], monospace 13px: [textarea], sans-serif 13.3333px: [19 items], sans-serif 13px: [image]*/ } else if ('mac' == isOS && '12e7f88a' == hash) {notation = rfp_green /*-apple-system 13.3333px: [19 items], monospace 13.3333px: [date, datetime-local, time], monospace 13px: [textarea], sans-serif 13px: [image] */ } else if ('linux' == isOS) { if (isBB) { // BB14: due to font config aliases /*Arimo 13.3333px: [19 items], monospace 12px: [textarea], monospace 13.3333px: [date, datetime-local, time], sans-serif 13px: [image]*/ if ('edeba276' == hash) {notation = rfp_green} } else { /*monospace 12px: [textarea], monospace 13.3333px: [date, datetime-local, time], sans-serif 13.3333px: [19 items], sans-serif 13px: [image]*/ if ('99054729' == hash) {notation = rfp_green} } } else if ('android' == isOS && '0833dc19' == hash) {notation = rfp_green /*Roboto 13.3333px: [19 items], monospace 12px: [textarea], monospace 13.3333px: [date, datetime-local, time], sans-serif 13px: [image]*/ } } catch(e) { hash = e; data = zErrLog } addBoth(12, METRIC, hash, btn, notation, data) return } const get_woff2 = (METRIC) => new Promise(resolve => { // check let typeCheck = typeFn(window.FontFace) if ('undefined' == typeCheck) { addBoth(12, METRIC, typeCheck) return resolve() } else { try { const supportsWoff2 = (function(){ const font = new FontFace('t', 'url("data:font/woff2;base64,d09GMgABAAAAAADwAAoAAAAAAiQAAACoAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAALAogOAE2AiQDBgsGAAQgBSAHIBuDAciO1EZ3I/mL5/+5/rfPnTt9/9Qa8H4cUUZxaRbh36LiKJoVh61XGzw6ufkpoeZBW4KphwFYIJGHB4LAY4hby++gW+6N1EN94I49v86yCpUdYgqeZrOWN34CMQg2tAmthdli0eePIwAKNIIRS4AGZFzdX9lbBUAQlm//f262/61o8PlYO/D1/X4FrWFFgdCQD9DpGJSxmFyjOAGUU4P0qigcNb82GAAA") format("woff2")', {}); font.load().catch(err => { // NetworkError: A network error occurred. < woff2 disabled/downloadable | fonts blocked e.g. uBO // ReferenceError: FontFace is not defined < layout.css.font-loading-api.enabled addDisplay(12, METRIC, log_error(12, METRIC, err)) }) return font.status == 'loaded' || font.status == 'loading' })() let value = (supportsWoff2 ? zS : zF) addBoth(12, METRIC, value) return resolve() } catch(e) { addBoth(12, METRIC, e,'','', zErrLog) return resolve() } } }) const outputFonts = () => new Promise(resolve => { if (gLoad) { let strDisplay try { let aDisplay = [] fntCodes.forEach(function(code) { let codeString = String.fromCodePoint(code) aDisplay.push('<span class="s99 monospace smaller" dir="ltr">'+ code.slice(2) + sc +'</span> <span class="bold bigger"> '+ codeString +' </span>' ) }) strDisplay = aDisplay.join(' ') } catch(e) { strDisplay = '<span class="mono">'+ e+'</span>' } addDisplay(12, 'glyphs_visual', strDisplay) } if (gRun && sectionIgnore.includes('fonts')) {return resolve()} let METRICD = 'font_detection' sDetail[isScope][METRICD] = [] // reset/clear fntHealth = [] set_fntList() Promise.all([ get_document_fonts('document_fonts'), // sets fntDocEnabled get_script_defaults('script_defaults'), get_fonts('font_sizes', METRICD), // uses fntDocEnabled get_system_fonts('fonts_moz'), get_system_fonts('fonts_system'), get_widget_fonts('fonts_widget'), get_formats(), get_woff2('woff2'), get_graphite('graphite'), // uses fntDocEnabled ]).then(function(){ // allow more time for font async fallback let isLies = isDomRect == -1 Promise.all([ get_fonts_max('font_sizes_max', isLies), get_fonts_faces('font_faces',METRICD), get_glyphs('glyphs', isLies), get_textmetrics('textmetrics'), get_fonts_offscreen('font_offscreen', METRICD), ]).then(function(){ if (fntBtn.length) {addDisplay(12, 'fntBtn', fntBtn)} // enumerated fonts over all font tests let array = sDetail[isScope][METRICD] if (array.length) { // let color = 12 if (isGecko && isSmart && 3 == fntHealth.length) { color = 'bad' // if font_names, font_faces and font_offscreen are the same and green // i.e rfp_green x 3 or fpp_green x 3, bb_green x 3 //dedupe fntHealth = dedupeArray(fntHealth) if (1 == fntHealth.length) { let value = fntHealth[0] if (value == bb_green || value == rfp_green || value == fpp_green) {color = 'good'} } } array = dedupeArray(array); array.sort() sDetail[isScope][METRICD] = array addDisplay(12, METRICD, addButton(color, METRICD, array.length +' fonts')) } return resolve() }) }) }) countJS(12) ================================================ FILE: js/generic.js ================================================ 'use strict'; dom = getUniqueElements() function getUniqueElements() { const dom = document.getElementsByTagName('*') return new Proxy(dom, { get: function(obj, prop) {return obj[prop]}, set: function(obj, prop, val) {obj[prop].textContent = `${val}`; return true} }) } /*** GENERIC ***/ function measureFn(target, metric) { let range, method, type = isDomRect //type = 2 // test try { if (runSE) {foo++} if (type > 1) {range = document.createRange(); range.selectNode(target)} if (type < 1) {method = target.getBoundingClientRect() // get a result regardless } else if (type == 1) {method = target.getClientRects()[0] } else if (type == 2) {method = range.getBoundingClientRect() } else if (type > 2) {method = range.getClientRects()[0] } return method } catch(e) { return {'error': true, 'errorstring': e+''} } } const newFn = x => typeof x != 'string' ? x : new Function(x)() function nowFn() {if (isPerf) {return performance.now()}; return} function rnd_string() {return Math.random().toString(36).substring(2, 15)} function rnd_number() {return Math.floor((Math.random() * (99999-10000))+10000)} function rnd_word(len = 5) { let str = '' for (let i=0; i < len; i++) {str += Math.floor(Math.random() * 25 + 10).toString(36)} return str } function removeElementFn(id) {try {dom[id].remove()} catch {}} function addProxyLie(value) {sData[SECT99].push(value)} function isProxyLie(value) { // ensure we only _use_ tampering in gecko smart mode // sect99 is now populated based on isProtoProxy which includes all gecko and can include other select engines // isSmart and isSmartDataMode are reset each run in smartFn if (isSmart || isSmartDataMode) {return sData[SECT99].includes(value)} return false } function smartFn(type) { // reset isFile = 'file:' == location.protocol isSmartDataMode = false isSmart = false // calculate if (isGecko && isVer >= isSmartMin) { if ('early' == type) { // we do not know isBB yet if (isSmartAllowed) {isSmart = true} } else { // must be smart: but we can create blocks, set criteria for what to allow // e.g. to block TB15 due to too much noise during development // make sure to cater for isSmartAllowed so it can be overriden via console for reruns //if (isSmartAllowed || !isTB) {isSmart = true} // example isSmart = true } //console.log(type, isSmart) isSmartDataMode = !isSmart // isSmartDataMode must be the opposite if ('final' == type && isSmartDataMode) {run_basic('data-only')} } } function typeFn(item, isSimple = false) { // return a more detailed result let type = typeof item if ('string' === type) { if (!isSimple) { if ('' === item) {type = 'empty string'} else if ('' === item.trim()) {type = 'whitespace'} } } else if ('number' === type) { if (Number.isNaN(item)) {type = 'NaN'} else if (Infinity === item) {type = 'Infinity'} } else if ('object' === type) { if (Array.isArray(item)) { type = 'array' if (!isSimple && !item.length) {type = 'empty array'} } else if (null === item) {type = 'null' } else { if (!isSimple) { try {if (0 === Object.keys(item).length) {type = 'empty object'}} catch {} } } } // do nothing: undefined, bigint, boolean, function return type +'' // make sure we return a string } function testtypeFn(isSimple = false) { let bigint = 9007199254740991 try {bigint = BigInt(9007199254740991)} catch(e) {} let data = ['a','',' ', 1, 1.2, Infinity, NaN, [], [1], {}, {a: 1}, null, true, false, bigint, undefined, function foobar() {},] data.forEach(function(item) { let type = typeFn(item, isSimple) console.log(typeof type, item, type) }) } function dedupeArray(array, toString = false) { array = array.filter(function(item, position) {return array.indexOf(item) === position}) if (toString) {return array.join(', ')} return array } function run_block(trace) { console.log(trace, 'blocking') log_perf(SECTG, 'isBlock','') isStop = true // prevent further code try { dom.tzpContent.style.display = 'none' dom.blockmsg.style.display = 'block' let msg = 'TZP requires gecko '+ isBlockMin +'+' if ('iframe' == trace) { msg = 'i\'m in an iframe' } else if ('insecure' == trace) { msg = 'i\'m in an insecure context' } else if ('quirks' == trace) { msg = 'i\'m in quirks mode - try again' } else if (isAllowNonGecko) { if (undefined !== isEngine) { msg = 'update your '+ isEngine +' browser' } else if (!isGecko) { msg = 'TZP requires gecko '+ (isEngineStr.includes(' or ') ? ', ' : ' or ') + isEngineStr } } dom.blockmsg.innerHTML = "<center><br><span style='font-size: 14px;'><b>"+ (isGecko ? 'Gah.' : 'Aw, Snap!') +"<br><br>" + msg +'<b></span></center>' } catch(e) {} } function run_basic(str = 'basic') { // basic mode: colors: gecko only, let other engines have some color if (isGecko && 'basic' == str) { log_perf(SECTG, 'isBasic','') for (let i=1; i < 19; i++) { document.documentElement.style.setProperty('--test'+i, 'var(--txtbasic)') document.documentElement.style.setProperty('--bg'+i, 'var(--bg99)') } document.documentElement.style.setProperty('--testweight', 'normal') document.documentElement.style.setProperty('--bggood', 'none') document.documentElement.style.setProperty('--bgbad', 'none') document.body.style.setProperty('--testbad', 'var(--txtbasic)') } // basic/other modes: notation if (str.length) { if ('undefined' == isEngine) {str = 'experimental'} else {str += ' mode'} let items = document.getElementsByClassName('nav-down') for (let i=0; i < items.length; i++) { // find '<a href' to end, prepend span // e.g. '<a href="#uad">▼</a>' -> '<span class="perf">notation</span><a href="#uad">▼</a>' let link = items[i].innerHTML link = link.slice(link.indexOf('<a href'), link.length) items[i].innerHTML = "<span class='perf'>"+ str +'</span> '+ link } } } function getElementProp(sect, id, name, pseudo = ':after') { // default none: https://www.w3.org/TR/CSS21/generate.html#content //console.log(sect, id, pseudo) try { let item = window.getComputedStyle(document.querySelector(id), pseudo).content // if supported but css blocked we get 'none' but if not supported (e.g. servo during development) we get '' // we want the FP css metrics to reflect what the css actually says, so match the defaults if ('undefined' == isEngine && '' == item) { // out of range: screen, inner, dpi: default is '' but we return '?' if ('#S' == id || '#D' == id || '#P' == id) {return '?'} // deviceposture, orientations, aspect ratios, display-mode let aUndefined = ['#cssDP','#cssOm','#cssDAR','#cssO','#cssAR','#cssDM'] if (aUndefined.includes(id)) {return 'undefined'} // everything else return zNA } if (runSI && !runSL) {item = 'none'} // don't error if runSL let typeCheck = typeFn(item, true) if ('string' !== typeCheck) {throw zErrType + typeCheck} item = item.replace(/"/g,'') // trim quote marks // screen(S) + window(D) + dpi(P:before) return none or ? when out of range if ('#S' == id || '#D' == id || '#P' == id) { if (':after' == pseudo && ' x ' == item.slice(0,3)) {item = item.slice(3)} // S/D remove leading ' x ' if ('none' == item || '' == item) {item = '?'} // return consistent ? for out of range/blocked } else if ('#cssVS' == id) { if (':after' == pseudo && ' x ' == item.slice(0,3)) {item = item.slice(3)} // remove leading ' x ' } // everything else should have a value: so "none" means css was blocked if ('none' == item) {throw zErrInvalid +"got 'none'"} // our css rules use "none " (trailing space) so we can detect when the css blocked // trim it for our return to compare to matchMedia if ('none ' == item) {item = 'none'} // return numbers if ('string' === typeCheck && !Number.isNaN(item * 1)) {item = item * 1} return item } catch(e) { log_error(sect, name, e) return zErr } } function mini(str) { // https://stackoverflow.com/a/22429679 const json = `${JSON.stringify(str)}` let len = json.length, hash = 0x811c9dc5 for (let i=0; i < len; i++) { hash = Math.imul(31, hash) + json.charCodeAt(i) | 0 } return ('0000000' + (hash >>> 0).toString(16)).slice(-8) } const promiseRaceFulfilled = async ({ promise, responseType, // promise response type limit = 1000 // default ms to fulfill }) => { // set up promise race const slowPromise = new Promise(resolve => setTimeout(resolve, limit)) // await promise race status const response = await Promise.race([slowPromise, promise]) // fastest will win .then(response => response instanceof responseType ? response : 'pending') .catch(error => 'rejected') return ( response == 'rejected' || response == 'pending' ? undefined : response ) } /*** GLOBAL ONCE ***/ function get_isArch(METRIC) { // FYI: 32bit no longer supported for linux FF145+ let t0 = nowFn(), value try { if (runSG) {foo++} let test = new ArrayBuffer(Math.pow(2,32)) // 4294967296 value = 64 } catch(e) { if ('blink' == isEngine && 'RangeError: Array buffer allocation failed' == e+'') { // chrome limits ArrayBuffer to 2145386496: https://issues.chromium.org/issues/40055619 isArch = zNA; value = zNA } else { isArch = log_error(3, 'browser_architecture', e, isScope, true) // persist sect3 value = zErr } } log_perf(SECTG, METRIC, t0,'', value) } function get_isAutoplay(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getAutoplayPolicy // get non-user-gesture values once let t0 = nowFn() try { if ('undefined' == typeof navigator.getAutoplayPolicy) { isAutoPlay = 'undefined' } else { let aTest, mTest let aPolicy = navigator.getAutoplayPolicy('audiocontext') try { if (runSG) {foo++} aTest = navigator.getAutoplayPolicy(dom.tzpAudio) } catch(e) { log_error(13, METRIC +'_audio', e, isScope, true) // persist sect13 aTest = zErr } let mPolicy = navigator.getAutoplayPolicy('mediaelement') try { if (runSG) {bar++} mTest = navigator.getAutoplayPolicy(dom.tzpVideo) } catch(e) { log_error(13, METRIC +'_media', e, isScope, true) // persist sect13 mTest = zErr } // combine isAutoPlay = (aPolicy === aTest ? aPolicy : aPolicy +', '+ aTest) +' | '+ (mPolicy === mTest ? mPolicy : mPolicy +', '+ mTest) } } catch(e) { isAutoPlay = zErr isAutoPlayError = log_error(13, METRIC, e, isScope, true) // persist sect13 } log_perf(SECTG, 'isAutoPlay', t0,'', (isAutoPlay == zErr ? zErr : '')) return } const get_isBB = (METRIC) => new Promise(resolve => { if (!isGecko) {return resolve()} let t0 = nowFn(), isDone = false setTimeout(() => { if (!isDone) { log_error(3, METRIC, zErrTime, isScope, true) // persist sect3 log_alert(SECTG, METRIC, zErrTime, isScope, true) log_perf(SECTG, METRIC, t0,'', zErrTime) resolve() } }, 150) let count = 0, expected = 1 function exit(value, id) { count++ if ('aboutTor' == id) {removeElementFn(id)} //console.log(`${count} of ${expected}: ${value}, ${id}`) // return on first true if (!isDone && true === value) { isDone = true //console.log('resolving after first success: test return no.', count, id) isBB = true if (id.includes('mullvad')) {isMB = true} else {isTB = true} // tidy notation if (isMB) { bb_green = sgtick+'MB]'+sc bb_red = sbx+'MB]'+sc bb_slider_red = sbx+'MB Slider]'+sc bb_standard = sg+'[MB Standard]'+sc bb_safer = sg+'[MB Safer]'+sc } log_perf(SECTG, METRIC, t0,'', (isMB ? 'mullvad': 'tor') +' browser | '+ id) resolve() } // otherwise if !isBB we exit false after expected number of test(s) if (!isBB && count == expected || zErr == value) { isDone = true log_perf(SECTG, METRIC, t0,'', (zErr == value ? zErr : false)) resolve() } } // FF121+: 1855861 const get_event = (el, id) => { el.onload = function() {exit(true, id)} el.onerror = function() {exit(false, id)} } if (!runSG) { try { // min ver is 128 now let list = [ 'content/torconnect/tor-connect.svg', // TB13.5 'skin/icons/torbrowser.png', // TB14.5 'skin/icons/mullvadbrowser.png', // MB14.5 ] expected = list.length list.forEach(function(image) { let parts = (image.slice(0,-4)).split('/') let id = parts[parts.length - 1] let el = new Image() el.src = 'chrome://global/' + image get_event(el, id) }) } catch(e) { // catch any unexpected extension fuckery log_error(3, METRIC, e, isScope, true) // persist sect3 log_alert(SECTG, METRIC, e.name, isScope, true) exit(zErr) } } }) const get_isBrave = (METRIC) => new Promise(resolve => { if ('blink' !== isEngine) return resolve() let t0 = nowFn() function exit(value = false) { log_perf(SECTG, METRIC, t0, '', value) return resolve() } try { // first determine isBrave // keeping in mind that extensions etc could mess with these if ('object' == typeof opr) return resolve() // opera // navigator let braveInN = 'brave' in navigator && Object.getPrototypeOf(navigator.brave).constructor.name == 'Brave' && navigator.brave.isBrave.toString() == 'function isBrave() { [native code] }' && 'brave' in navigator ? Object.keys(Object.getOwnPropertyDescriptors(Navigator.prototype)).indexOf("brave") < 20 : false if (!braveInN) { exit() } else { // userAgentData Promise.all([ get_agent_data('', isOS, false) ]).then(function(res){ try { // we already have braveInN, now we want braveInU let data = res[0] if ('object' !== typeof data) {exit()} // the position of 'Brave' can/might vary (seems like a patch not randomizing) // so we need to loop: assumes same position in brand + fullVersionList for (let i = 0; i < data.brands.length; i++) { if ('Brave' == data.brands[i].brand && 'Brave' == data.fullVersionList[i].brand) { isBrave = true; break } } // if isBrave, we can check if FP protection is enabled: telltale signs we can easily check include // null keyboard // gibberish in plugins (if pdf enabled) // canvas // other possibles are: tiny chrome + tiny screen positions // others: extendedscreen can't be true // plugins gibberish // items are always in a set order (check each platform) - brave mixes this ip // one of the five is always missing - brave tends to overwrite one of them // non expected items have no spaces e.g. /* examples "aZMOPuXT: qdWTwBAIjRnTRQnb: WyhQIMtePHDBnyCJMtWyhYrdWTw3j47d", 8, 16, 32 "4k5k5cO: BMteXyhQQv268mb: FCgYzCBn6dt15FCo7GDJEKFpct9mbse", 7, 15, 31 */ let aOrder = ['PDF Viewer','Chrome PDF Viewer','Chromium PDF Viewer','Microsoft Edge PDF Viewer','WebKit built-in PDF'] // 2 out of 3 should be enough to determine gibberish exit(isBrave + (isBrave ? ' '+ isBraveSmart : '')) } catch(e) { log_error(3, METRIC, e, isScope, true) // persist sect3 exit(zErr) } }) } } catch(e) { log_error(3, METRIC, e, isScope, true) // persist sect3 exit(zErr) } }) function get_isDevices() { isDevices = undefined let t0 = nowFn() try { if (undefined !== navigator.mediaDevices) {return} if (runSG) {foo++} navigator.mediaDevices.enumerateDevices().then(function(devices) { isDevices = devices if (gLoad) {log_perf(SECTG, 'isDevices', t0,'', nowFn())} } )} catch(e) {} } function get_isEngine(METRIC) { if (isGecko) {return} let t0 = nowFn() try { let oEngines = { blink: [ 'number' === typeof TEMPORARY, 'number' === typeof PERSISTENT, 'object' === typeof onappinstalled, 'object' === typeof onbeforeinstallprompt, 'function' === typeof webkitResolveLocalFileSystemURL, ], webkit: [ 'object' === typeof browser, 'object' === typeof safari, 'function' === typeof webkitConvertPointFromNodeToPage, 'function' === typeof webkitCancelRequestAnimationFrame, 'object' === typeof webkitIndexedDB, ], /* ignore edgeHTML edgeHTML: [ 'function' === typeof clearImmediate, 'function' === typeof msWriteProfilerMark, 'object' === typeof oncompassneedscalibration, 'object' === typeof onmsgesturechange, 'object' === typeof onmsinertiastart, 'object' === typeof onreadystatechange, 'function' === typeof setImmediate, ] //*/ } // array engine matches, so subsequent results doesn't override prev let aEngine = [], aAllowed = [] for (const engine of Object.keys(oEngines).sort()) { aAllowed.push(engine) let sumE = oEngines[engine].reduce((prev, current) => prev + current, 0) if (sumE > (oEngines[engine].length/2)) {aEngine.push(engine)} } aAllowed.sort() isEngineStr = aAllowed.join(', ') if (aAllowed.length > 1) { isEngineStr = ' or '+ aAllowed[aAllowed.length - 1] aAllowed = aAllowed.slice(0,-1) isEngineStr = aAllowed.join(',') + isEngineStr } if (aEngine.length == 1) {isEngine = aEngine[0]} // valid one result // set minimum if (undefined !== isEngine) { // approved engine detected, if not enforcing min, disable block isEngineBlocked = isAllowNonGeckoMin // if enforcing min, check if (isAllowNonGeckoMin) { try { if ('blink' == isEngine) { // 109 is the last version supported on win7 //if ('function' == typeof(Map.groupBy)) {isEngineBlocked = false} // 117 2023-Sept if ('function' == typeof(Document.parseHTMLUnsafe)) {isEngineBlocked = false} // 124 2024-Apr //if ('function' !== typeof(Intl.DurationFormat)) {isEngineBlocked = false} // 129 2024-Sep } else if ('webkit' == isEngine) { // https://en.wikipedia.org/wiki/Safari_(web_browser)#Version_compatibility // 15.6.1 2022-Aug = last version supported on macOS 10.15? if ('function' == typeof(Intl.DurationFormat)) {isEngineBlocked = false} // 16.4 2023-Mar //if ('function' == typeof(Map.groupBy)) {isEngineBlocked = false} // 17.4 2024-Mar } } catch(e) {} } } else if (isAllowNonGeckoUndefined) { isEngine = 'undefined' // a string vs undefined typeof isEngineBlocked = false } } catch(e) {} log_perf(SECTG, METRIC, t0,'', isEngine) } const get_isFileSystem = (METRIC, isWarmup = false) => new Promise(resolve => { // meta: 1748667 // note: pref change (dom.fs.enabled) requires new reload: so we can run once let t0 = nowFn() function exit(value) { if (!isWarmup) { isFileSystem = value log_perf(SECTG, METRIC, t0,'', value) } return resolve() } if (navigator.storage == undefined || 'function' !== typeof navigator.storage.getDirectory) { exit(zD) } Promise.all([ navigator.storage.getDirectory() ]).then(function(){ exit(zE) }) .catch(function(e){ isFileSystemError = e+'' exit(zErr) }) }) const get_isFontDelay = () => new Promise(resolve => { if (!isBB || !isGecko || isVer < 139 || 'android' == isOS) {return resolve()} //if ('windows' !== isOS && 'mac' !== isOS) {return resolve()} // ^ linux shouldn't have any delay since it's dets the font dir as system font dir? // ^ but for now lets treat all desktop as suspect and force TB tro address this perf cliff // BB: font.vis + bundled fonts // very slow to async fallback | on linux the bundled fonts IS the system font dir and is not affected // not the case when using font.system.whitelist | we should see if we can get this fixed upstream // currently only nightly uses font.vis but could change at any time and is not version specific // detect if we need a delay by testing if fontface is working isFontDelay = true // BB default delay in case of errors/fuckery Promise.all([ get_fonts_faces('','', ['Arial','Courier','Times New Roman']), // all are allowed + expected in windows/mac ]).then(function(res){ // either 'error', 'none' or an array of detected fonts //console.log(res[0]) if ('none' == res[0]) {isFontDelay = false} // only remove delay if 'none': errors/detected-fonts = force a delay return resolve() }) }) function get_isGecko(METRIC) { let t0 = nowFn(), value try { let list = [ [DataTransfer, 'DataTransfer', 'mozSourceNode'], [Document, 'Document', 'mozFullScreen'], [HTMLCanvasElement, 'HTMLCanvasElement', 'mozPrintCallback'], [HTMLElement, 'HTMLElement', 'onmozfullscreenerror'], [HTMLVideoElement, 'HTMLVideoElement', 'mozDecodedFrames'], [IDBIndex, 'IDBIndex', 'mozGetAllKeys'], [IDBObjectStore, 'IDBObjectStore', 'mozGetAll'], [Screen, 'Screen', 'mozOrientation'], // 1325110: mozOrientation slated for deprecation [SVGElement, 'SVGElement', 'onmozfullscreenchange'] ] let obj, prop, aNo = [] list.forEach(function(array) { obj = array[0] prop = array[2] if ('function' === typeof obj && ('object' === typeof Object.getOwnPropertyDescriptor(obj.prototype, prop))) { } else { aNo.push(array[1]) } }) let found = (list.length - aNo.length) if (found > 5) { isGecko = true // alert if any gecko checks fail if (aNo.length) {log_alert(SECTG, METRIC, aNo.join(', '), isScope, true)} } value = isGecko +' | '+ found +'/'+ list.length } catch(e) { value = zErr } log_perf(SECTG, METRIC, t0,'', value) return } function get_isPointerRawUpdate(event) { let value = 'undefined' try { if (event.getCoalescedEvents && event.getCoalescedEvents().length > 1) { //console.log("Coalesced events:", event.getCoalescedEvents()); } else { value = {'persistentDeviceId': event.persistentDeviceId, 'pointerType': event.pointerType} } } catch(e) { value = zErr } isPointerRawUpdate = value } const get_isOS = (METRIC) => new Promise(resolve => { let t0 = nowFn() // 1. widget font: mac/linux function trywidget() { try { if (runSG) {foo++} let aIgnore = [ 'cursive','emoji','fangsong','fantasy','math','monospace','none','sans-serif', 'serif','system-ui','ui-monospace','ui-rounded','ui-serif','undefined' ] let font = getComputedStyle(dom.tzpbutton).getPropertyValue('font-family') if ('string' !== typeFn(font) || aIgnore.includes(font)) { throw zErr } else { if (isGecko) { // button if (font.slice(0,12) == "MS Shell Dlg") {exit('windows') } else if (font == '-apple-system') {exit('mac') } else {throw zErr} } else { // mac webkit // search and select return -apple-system // mozfonts (e.g. mozbutton) return webkit-standard // status-bar returns -apple-status-bar // menu returns -apple-menu tryfonts() } } } catch { tryfonts() } } // 2: fonts function tryfonts() { // check doc fonts let fntEnabled = false try { if (runSG) {foo++} let fntTest = '\"test font name\"' //dom.tzpDocFont.style.fontFamily = fntTest let font = getComputedStyle(dom.tzpDocFont).getPropertyValue('font-family'), fontnoquotes = font.slice(0, fntTest.length - 2) // ext may strip quotes marks fntEnabled = (font == fntTest || fontnoquotes == fntTest ? true : false) } catch {} if (!fntEnabled) {trysomethingelse(); return} // check fonts get_fonts_size(false).then(res => { if ('object' == typeFn(res, true)) { let aDetected = [], found for (const k of Object.keys(res)) {aDetected.push(k)} if (isGecko) { found = aDetected[0] if (aDetected.length == 1) { if (found == 'MS Shell Dlg \\32') {exit('windows') } else if (found == '-apple-system') {exit('mac') } else if (found == 'Dancing Script') { exit('android') } else { trysomethingelse() } //console.log('isOS font check', found, isOS) } else if (isGecko && aDetected.length == 0) { exit('linux') } else { trysomethingelse() } } else { console.log(aDetected) // if we detected the fake font then ignore //aDetected.push('--00'+ rnd_string()) // test let aFake = aDetected.filter(x => !fntMaster.platform.all.includes(x)) if (aFake.length) {trysomethingelse(); return} // get counts let aWindows = aDetected.filter(x => fntMaster.platform.windows.includes(x)), aMac = aDetected.filter(x => fntMaster.platform.mac.includes(x)), aAndroid = aDetected.filter(x => fntMaster.platform.android.includes(x)) let intW = aWindows.length, intM = aMac.length, intA = aAndroid.length //console.log(aWindows, intW, aMac, intM, aAndroid, intA) if (intW > 0 && (intM + intA == 0)) {exit('windows') } else if (intM > 0 && (intW + intA == 0)) {exit('mac') } else if (aDetected.length == 0) {exit('linux') } else { //} else if (intA > 0 && (intM + intW == 0)) {exit('android') //} else {exit('linux')} // can't base android vs lionux on a single font trysomethingelse() } } } else { trysomethingelse() } }) } // 3. now what? function trysomethingelse() { exit() } // 4. exit function exit(value) { isOS = value isDesktop = 'android' !== isOS dom.tzpResource.style.backgroundImage = "url('chrome://branding/content/" + (isDesktop ? '' : 'fav') + "icon64.png')" // set icon log_perf(SECTG, METRIC, t0, '', isOS +'') if (undefined == isOS) { // for now only alert isGecko if (isGecko) { isOSErr = log_error(3, "os", zErrType +'undefined', isScope, true) // persist sect3 log_alert(SECTG, METRIC, "undefined", isScope, true) } } return resolve() } if (isGecko) { trywidget() } else { set_fntList_mini() tryfonts() /* // get svh and lvh: if they differ then you have a dynamic urlbar // this is fast - could we leverage it for gecko as well // maybe not since isBB might restrict it for FPing dynamic urlbar // also apps may allow disabling it // also maybe apps will enable it on other devices/tablets/platforms // or extensions might tamper with it // so for now just record the info for non-gecko let aList = ['L','S'] try { let data = {} aList.forEach(function(k) {data[k] = dom['tzp'+ k +'V'].offsetHeight}) let diff = Math.abs(data['L'] - data['S']) if (diff > 20) { // allow some wriggle room //if ('blink' == isEngine) {isOS = 'android'} } log_perf(SECTG, METRIC, t0, '', 'L: '+ data['L'] +' | S: '+ data['S']) } catch(e) {} //*/ return resolve() } }) const get_isRecursion = () => new Promise(resolve => { // 2nd test is more accurate/stable const METRIC = "isRecursion" let t0 = nowFn() let level = 0 function recurse() {level++; recurse()} try {recurse()} catch {} level = 0 try { recurse() } catch(e) { let stacklen = e.stack.toString().length // display value isRecursion = [level +" [stack length: "+ stacklen +']'] log_perf(SECTG, METRIC, t0, "", isRecursion.join()) // metric values: only collect level // https://github.com/arkenfox/user.js/issues/1789: round down to 1000's isRecursion.push(Math.floor(level/1000)) return resolve() } }) function get_isStylesheet(end = 7680, start = 200) { // currently the ranges for both screen (min-device-*) and // inner (min-*) are 400-2560 and the two css files total almost 500kb let t0 = nowFn(), state = start +'-'+ end, METRIC = 'isStylesheet' try { const style = document.createElement('style') style.type = 'text/css' let rules = [] function add_blank(i) { rules.push("@media (min-device-width:"+ i +"px){#S:before{content:'';}}") rules.push("@media (min-device-height:"+ i +"px){#S:after{content:'';}}") rules.push("@media (min-width:"+ i +"px){#D:before{content:'';}}") rules.push("@media (min-height:"+ i +"px){#D:after{content:'';}}") } add_blank(start - 1) for (let i= start; i <= end; i++) { rules.push("@media (min-device-width:"+ i +"px){#S:before{content:'" + i +"';}}") rules.push("@media (min-device-height:"+ i +"px){#S:after{content:' x " + i +"';}}") rules.push("@media (min-width:"+ i +"px){#D:before{content:'" + i +"';}}") rules.push("@media (min-height:"+ i +"px){#D:after{content:' x " + i +"';}}") } add_blank(end + 1) style.appendChild(document.createTextNode(rules.join(' '))) document.head.appendChild(style) isStylesheet = state } catch(e) { state = zErr log_error(SECTG, METRIC, e, isScope, true) } // ~65ms for 7680 (8k) cold | reload ~15ms // some/most of this time is spent awaiting more js files anyway log_perf(SECTG, METRIC, t0, '', state) return } const get_isSystemFont = () => new Promise(resolve => { //if (!isGecko) {return resolve()} let t0 = nowFn() function exit(value) { log_perf(SECTG, 'isSystemFont', t0,'', value) return resolve() } let aMoz = [ // -moz seem to always be the same '-moz-bullet-font','-moz-button','-moz-button-group','-moz-desktop','-moz-dialog','-moz-document', '-moz-field','-moz-info','-moz-list','-moz-message-bar','-moz-pull-down-menu','-moz-window','-moz-workspace', ] let aNonMoz = [ // in gecko non -moz seem to always be the same: mac might differ I seem to recall this // in the past - anyway we grab the first of each e.g. caption + menu differ in blink (windows) 'caption','icon','menu','message-box','small-caption','status-bar', ] // first aFont per computed family // add '-default-font' (alphabetically first) so it's easy to see what it pairs with in baseFonts let aFonts = ['-default-font'] if (isGecko) {aFonts = aFonts.concat(aMoz)} aFonts = aFonts.concat(aNonMoz) aFonts.sort() try { let el = dom.tzpDiv, data = [] aFonts.forEach(function(font){ el.style.font ='' // always clear in case a font is invalid/deprecated el.style.font = font let family = getComputedStyle(el)['font-family'] if (!data.includes(family)) { data.push(family) isSystemFont.push(font) } }) if (isGecko) { // we use isSystemFont in fntSizes where -moz group doesn't match non -moz even though all of // aFonts have the same computedStyes: ensure we have one of each if (0 == isSystemFont.filter(x => aMoz.includes(x).length)) {isSystemFont.push(aMoz[0])} } isSystemFont.sort() exit(isSystemFont.join(', ')) } catch(e) { exit(e.name) // log nothing: we run in fonts later } }) function get_isVer(METRIC) { if (!isGecko) {return} let t0 = nowFn() isVer = cascade() if (isVer == 152) {isVerExtra = '+'} else if (isVer == 127) {isVerExtra = ' or lower'} log_perf(SECTG, METRIC, t0,'', isVer + isVerExtra) // gecko block mode isBlock = isVer < isBlockMin if (isBlock) {run_block('gecko'); return} // sets isStop // set smarts / modes smartFn('early') if (!isSmart && isVer < isSmartMin) {run_basic()} return function cascade() { let test try { // old-timey check: avoid false postives: must be 128 or higher try {let test128 = (new Blob()).bytes()} catch {return 127} // 1896509 // now cascade try {if (SVGTextPathElement.prototype.hasOwnProperty('side')) return 152} catch(e) {} // 2034371 if (CSSContainerRule.prototype.hasOwnProperty('conditions')) return 151 // 2022827 if ('object' == typeof visualViewport.onscrollend) return 150 // 1801658 try {Temporal.PlainDate.from({calendar:'gregory', monthCode:'M12', month:13, year:2019, day:1})} catch(e) {if ('RangeError' == e.name) return 149} // 2009792 // 148: fast-path: pref dom.location.ancestorOrigins.enabled: default true 148+ try {if (undefined !== location.ancestorOrigins) return 148} catch(e) {} // 1085214 try {let test148 = new Temporal.Duration(0).total({unit:'years', relativeTo:'-271821-04-19'}); return 148} catch(e) {} // 2004851 if (Intl.supportedValuesOf('numberingSystem').includes('tols')) return 147 // 2000225 ? try {throw new DOMException('a', 'b')} catch(e) {if (0 !== e.columnNumber) return 146} // 1997216 if (undefined !== (new ToggleEvent('toggle', null)).source) return 145 // 1968987 if (undefined == window.CSS2Properties) return 144 // 144: 1919582 // 143: fast-path: pref: layout.css.moz-appearance.webidl.enabled: default false 143+ if (!CSS2Properties.prototype.hasOwnProperty('-moz-appearance')) return 143 // 1977489 try { let segmenter = new Intl.Segmenter('en', {granularity: 'word'}) test = Array.from(segmenter.segment('a:b')).map(({ segment }) => segment) if (3 == test.length) return 142 // 1960300 } catch(e) {} // 141: fast-path: requires temporal default enabled FF139+ javascript.options.experimental.temporal try {if (undefined == Temporal.PlainDate.from('2029-12-31[u-ca=gregory]').weekOfYear) return 141} catch(e) {} // 1950162 // 141: fast-path: dom.intersection_observer.scroll_margin.enabled (default true) try {if (window["IntersectionObserver"].prototype.hasOwnProperty('scrollMargin')) return 141} catch(e) {} // 1860030 // 140: fast-path: pref: dom.event.pointer.rawupdate.enabled : default true 140+ try {if ("object" === typeof onpointerrawupdate) return 140 } catch(e) {} // 1550462 // 140: if < 141 there is only one paint entry "PerformancePaintTiming" try {if (undefined !== performance.getEntriesByType("paint")[0].presentationTime) return 140} catch(e) {} // 1963464 try {if ('' !== dom.tzpAudio.preload) return 140} catch(e) {} // 929890 // 139 if (HTMLDialogElement.prototype.hasOwnProperty('requestClose')) return 139 // 1960556 // 138: fast-path: requires webrtc e.g. media.peerconnection.enabled | --disable-webrtc try {if (RTCCertificate.prototype.hasOwnProperty('getFingerprints')) return 138} catch(e) {} // 1525241 // 138: fast-path: dom.origin_agent_cluster.enabled if ('boolean' == typeof originAgentCluster) return 138 // 1665474 // 138: must be FF134 or higher try { if (HTMLScriptElement.prototype.hasOwnProperty('textContent')) { // FF135+ test = Intl.NumberFormat('yo-bj', {style: 'unit', unit: 'year', unitDisplay: 'narrow'}).format(1) if ('606d1046' == mini(test)) return 138 // 1954425 } } catch(e) {} // 137 fast-path: javascript.options.experimental.math_sumprecise if ('function' == typeof Math.sumPrecise) return 137 // 1943120 // 136 fast-path: FF132+ pref enabled javascript.options.experimental.regexp_modifiers try {if ((new RegExp("(?i:[A-Z]{4})")).test('abcd')) return 136} catch {} // 1939533 if (HTMLScriptElement.prototype.hasOwnProperty('textContent')) return 135 // 1905706 // 134: may be affected by --with-system-icu // ToDo: replace, fallbacks? if ('lij' == Intl.PluralRules.supportedLocalesOf('lij').join()) return 134 // 1927706 try { let parser = (new DOMParser).parseFromString("<select><option name=''></option></select>", 'text/html') if (null === parser.body.firstChild.namedItem('')) return 133 // 1837773 } catch {} try { const re = new RegExp('(?:)', 'gv'); test = RegExp.prototype[Symbol.matchAll].call(re, '𠮷') for (let i=0; i < 3; i++) {if (true == test.next().done) return 132} // 1899413 } catch {} try { test = new Intl.DateTimeFormat('zh', {calendar: 'chinese', dateStyle: 'medium'}).format(new Date(2033, 9, 1)) if ('2033' == test.slice(0,4)) return 131 // 1900196 } catch {} try {new RegExp('[\\00]','u')} catch(e) {if (e+'' == 'SyntaxError: invalid decimal escape in regular expression') return 130} // 1907236 if (CSS2Properties.prototype.hasOwnProperty('WebkitFontFeatureSettings')) return 129 // 1595620 return 128 } catch(e) { console.error(e) return 0 } } } const get_isXML = () => new Promise(resolve => { // get once ASAP +clear console: not going to change between tests // gecko change app lang and it requires closing and a new tab // blink changing app lang also asks for a relaunch // note: blink doesn't seem to translate these, or it's not tied to either app or web-content language // assume webkit requires an app restart let t0 = nowFn(), delimiter = ':' const list = { n02: 'a', n03: '', n04: '<>', n05: '<', n07: '<x></X>', n08: '<x x:x="" x:x="">', n09: '<x></x><x>', n11: '<x>&x;', n14: '<x>&#x0;', n20: '<x><![CDATA[', n27: '<x:x>', n28: '<x xmlns:x=""></x>', n30: '<?xml v=""?>' } try { let parser = new DOMParser let isMethod = isGecko ? 'gecko' : 'other' // determine method to get values if engine is 'undefined' if ('undefined' == isEngine) { // servo fails in both //'gecko': can't access property "firstChild", target is undefined //'other': can't access property "innerText", target is undefined try { let testdoc = parser.parseFromString('a', 'application/xml') let testtarget = testdoc.getElementsByTagName('parsererror')[0] let teststr = testtarget.innerText } catch(e) { // failed 'other' method, so use 'gecko' method isMethod = 'gecko' } } for (const k of Object.keys(list)) { let doc = parser.parseFromString(list[k], 'application/xml') let target = doc.getElementsByTagName('parsererror')[0] //if ('n02' == k && !isGecko) {console.log(doc.getElementsByTagName('parsererror')[0])} //debug let str, value, parts if ('gecko' == isMethod) {str = target.firstChild.textContent} else {str = target.innerText} if (runST) {str =''} let typeCheck = typeFn(str) if ('string' !== typeCheck) {throw zErrType + typeCheck} if ('gecko' == isMethod) { // gecko //split into parts: works back to FF52 and works with LTR parts = str.split('\n') if ('n02' == k) { // ensure 3 parts: e.g. hebrew only has 2 lines let tmpStr = parts[1] let loc = window.location+'', locLen = loc.length, locStart = tmpStr.indexOf(loc) if (undefined == parts[2]) { let position = locLen+ locStart parts[1] = (tmpStr.slice(0, position)).trim() parts.push((tmpStr.slice(-(tmpStr.length - position))).trim()) } // set delimiter: should aways be the last item in parts[1] after we strip location // usually = ":" (charCode 58) but zh-Hans-CN = ":" (charCode 65306) and my = " -" let strLoc = (parts[1].slice(0, locStart)).trim() // trim delimiter = strLoc.slice(-1) // last char // concat some bits // don't trim strName prior to +delimiter (which is length 1) // e.g. 'fr','my' have a preceeding space, so capture that let strName = parts[0].split(delimiter)[0] + delimiter // use an object as joining for a string can get weird with RTL let oData = { 'delimiter': delimiter +' (' + delimiter.charCodeAt(0) +')', // redundant but record it for debugging 'error': strName, 'line': parts[2].trim(), 'location': strLoc, } isXML['n00'] = oData } // parts[0] is always the error message value = parts[0] let trimLen = parts[0].split(delimiter)[0].length + 1 value = value.slice(trimLen).trim() } else { // blink + webkit let newtarget = target.children // [0] "This page contains the following errors:" // [1] "error on line X at column Y: " + actual error // [2] "Below is a rendering of the page up to the first error." str = newtarget[1].textContent // cleanup english XML messages: I don't think blink or webkit translate these let isErrorStr = '', isError = 'error on line' == str.slice(0,13) if (isError) { let position = str.indexOf(": ") if (position > 0) { isErrorStr = str.slice(0,28) str = str.slice(position + 2) } } value = str.replace(/\n/g,'') if ('n02' == k) { if (isError) { isXML['n00'] = {0: newtarget[0].textContent, 1: isErrorStr, 2: newtarget[2].textContent} } else { isXML['n00'] = {0: newtarget[0].textContent, 2: newtarget[2].textContent} } } //if ('n02' == k) {console.log(str, '\n', newtarget)} //debug } isXML[k] = value } } catch(e) { isXML = e+'' } if (isGecko && gClear) {console.clear()} log_perf(SECTG, 'isXML', t0,'', ('string' == typeof isXML ? zErr : '')) return resolve() }) function get_isXSLT() { // FF151: dom.xslt.enabled // no need to record the error, returning n/a catches the entropy/change // the boolean can be used to determine health // note: this does not reflect the preference on reruns but rather the error // e.g. once enabled, the error is not thrown even if the pref is flipped to false isXSLT = isGecko ? true : false if (isGecko) { try { let x = new XSLTProcessor() } catch(e) { if ('ReferenceError: XSLTProcessor is not defined' == e+'') {isXSLT = false} } } return } /*** PREREQ ***/ function get_isDomRect() { if (!isGecko) {return} // like canvas: this is only testing for protection, so always run in gecko including basic mode // determine valid domrect methods + grab data for analysis let t0 = nowFn() const names = ['element_getbounding', 'element_getclient','range_getbounding','range_getclient'] const props = ['bottom','height','left','right','top','width','x','y'] // reset: assume lies isDomRect = -1 aDomRect = [false, false, false, false] oDomRect = {} let el = dom.tzpRect for (let i=0; i < 4; i++) { let METRIC = names[i] let tmpobj = {}, hash try { let obj if (0 == i) { obj = el.getBoundingClientRect() } else if (1 == i) { obj = el.getClientRects()[0] } else { let range = document.createRange() range.selectNode(el) obj = (2 == i ? range.getBoundingClientRect() : range.getClientRects()[0] ) } props.forEach(function(prop){ let value = obj[prop] if (runST && i == 0) {value = 's'} let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} if (runSL) {value += 0.1} tmpobj[prop] = value }) hash = mini(tmpobj) aDomRect[i] = ('642e7ef0' == hash) } catch(e) { log_error(15, 'domrect_'+ METRIC, e) aDomRect[i] = zErr tmpobj = zErr hash = zErr } if (undefined == oDomRect[hash]) { oDomRect[hash] = {'data': tmpobj, 'methods': [METRIC]} } else { oDomRect[hash]['methods'].push(METRIC) } } //aDomRect = [false, false, false, false] isDomRect = aDomRect.indexOf(true) //console.log(isDomRect, aDomRect) log_perf(SECTP, 'isDomRect', t0,'', aDomRect.join(', ')) return } function get_isPerf() { isPerf = false for (let i=1; i < 50 ; i++) { try { let value = Math.trunc(performance.now() - performance.now()) if (0 !== value && -1 !== value) {return} } catch {return} } isPerf = true } /** CLICKING **/ function copyclip(element) { if ('clipboard' in navigator) { try { let content = dom[element].innerHTML if ('metricsDisplay' == element) { content = dom['metricsTitle'].innerHTML +'\n\n'+ content } // remove spans, change linebreaks let regex = /<br\s*[\/]?>/gi content = content.replace(regex, '\r\n') content = content.replace(/<\/?span[^>]*>/g,'') // get it navigator.clipboard.writeText(content).then(function() { // indicate it try { let target = dom.metricsBtnCopy target.classList.add('indicate') target.classList.remove('btn0') setTimeout(function() { target.classList.add('btn0') target.classList.remove('indicate') }, 500) } catch(e) {} }, function() { // clipboard write failed }) } catch {} } } function showhide(id, style) { let items = document.getElementsByClassName('tog'+ id) for (let i=0; i < items.length; i++) {items[i].style.display = style} } function togglerows(id, word) { let items = document.getElementsByClassName('tog'+ id) let style = items[0].style.display == 'table-row' ? 'none' : 'table-row' for (let i=0; i < items.length; i++) {items[i].style.display = style} if ('btn' == word) { word = '['+ ('none' == style ? '+' : '-') +']' } else { word = ('none' == style ? '&#9660; show ' : '&#9650; hide ') + ('' == word || word === undefined ? 'details' : word) } try {dom['label'+ id].innerHTML = word} catch {} } /*** METRICS DISPLAY ***/ function json_highlight(json, clrValues = false) { let clrSymbols = false if ('health' == overlayName) { clrValues = false if ('_summary' == overlayHealth) {clrSymbols = true} } if ('string' !== typeof json) { // get the overlay width and use that to calculate a json maxlength // old hardcoded code: linux 88, android 68, windows/mac incl. BB = 95 let minLen = 50, len = isDesktop ? 95 : minLen overlayInfo = '' try { // we use the table width because it is visible // overlaycontent available width is 100px less let contentWidth = dom.tbfp.clientWidth - (100 + isScrollbar) len = (contentWidth/overlayCharLen) - 2 // give us some wiggle room if (len < minLen) {len = minLen} else {len = Math.floor(len)} overlayInfo = contentWidth +' | '+ overlayCharLen +' | '+ len let strLast = '1234567890', strSpacer = (' ').repeat(len -(overlayInfo.length + strLast.length)) overlayInfo += strSpacer + strLast } catch(e) {} json = json_stringify(json, len); } json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { var cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { if (clrValues) { cls = 'string'; } else if (clrSymbols) { cls ='' match = match.replace(tick, green_tick) match = match.replace(cross, red_cross) } else { return match } } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="'+ cls +'">'+ match +'</span>'; }) } function json_stringify(passedObj, overlayMaxLength = 95, options = {}) { /* https://github.com/lydell/json-stringify-pretty-compact */ const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g; const indent = JSON.stringify( [1], undefined, options.indent === undefined ? 2 : options.indent ).slice(2, -3); const maxLength = indent === '' ? Infinity : options.maxLength === undefined ? overlayMaxLength : options.maxLength; let { replacer } = options; return (function _stringify(obj, currentIndent, reserved) { if (obj && 'function' === typeof obj.toJSON) { obj = obj.toJSON() } const string = JSON.stringify(obj, replacer); if (string === undefined) { return string } const length = maxLength - currentIndent.length - reserved; if (string.length <= length) { const prettified = string.replace( stringOrChar, (match, stringLiteral) => { return stringLiteral || `${match} `; } ); if (prettified.length <= length) { return prettified; } } if (replacer != null) { obj = JSON.parse(string); replacer = undefined; } if ('object' == typeof obj && obj !== null) { const nextIndent = currentIndent + indent; const items = []; let index = 0; let start; let end; if (Array.isArray(obj)) { start = '['; end = ']'; const { length } = obj; for (; index < length; index++) { items.push( _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) || 'null' ); } } else { start = '{'; end = '}'; const keys = Object.keys(obj); const { length } = keys; for (; index < length; index++) { const key = keys[index]; const keyPart = `${JSON.stringify(key)}: `; const value = _stringify( obj[key], nextIndent, keyPart.length + (index === length - 1 ? 0 : 1) ); if (value !== undefined) { items.push(keyPart + value); } } } if (items.length > 0) { return [start, indent + items.join(`,\n${nextIndent}`), end].join( `\n${currentIndent}` ); } } return string; })(passedObj, '', 0); } function metricsAction(type) { if ('close' == type) { dom.modaloverlay.style.display = 'none' dom.overlay.style.display = 'none' dom.metricsDisplay.innerHTML = '' // clear so we always start at the top dom.overlayInfo.innerHTML = '' } else if ('console' == type) { if (metricsData !== undefined) {console.log(metricsTitle, metricsData)} } else if ('download' == type) { if (metricsData == undefined) {return} try { let name = metricsPrefix + (metricsTitle.replaceAll(': ', '_')).toLowerCase() var file = new Blob([JSON.stringify(metricsData, null, 2)], {type: 'application/json'}) var a = document.createElement('a') a.href = URL.createObjectURL(file) a.download = name a.click() } catch(e) { console.error(e) } } else { // user changed settings dom.metricsDisplay.innerHTML = '' // force delay so reflow = always start at the top setTimeout(function() { metricsShow(overlayName, overlayScope) }, 0) } } function metricsEvent(evt) { if ('block' !== dom.modaloverlay.style.display) {return} evt = evt || window.event var isEscape = false if ('key' in evt) { isEscape = ('Escape' == evt.key || 'Esc' == evt.key) } else { isEscape = (27 == evt.keyCode) } if (isEscape) {metricsAction('close') } else if ((evt.ctrlKey || evt.metaKey) && 67 == evt.keyCode) { copyclip('metricsDisplay') } } function metricsShow(name, scope) { overlayName = name let isKit = false if (isGecko & isSmart) { // atm this is pretty much limited to health, pixels_match, timezone_offsets_data // and fonts_detected try { let btn = dom[scope+name] if (undefined == btn) {btn = dom[name]} isKit = 'btngood' == btn.children[0].classList[0] } catch(e) {} } if (isKit) {dom.overlaykit.classList.remove('hidden')} else {dom.overlaykit.classList.add('hidden')} let isVisible = dom.modaloverlay.style.display == 'block' let aShowFormat = ['fingerprint','health'] // lies need to be made into an object with metrics as key let isSection = sectionNames.includes(name) let isShowFormat = aShowFormat.includes(name) || isSection let isHealth = name == 'health' let target = name, overlayScope = scope if (isShowFormat) {target = metricsUI(target, isVisible, isSection, isHealth)} let data, color = 99, filter = '' if (name == SECT97 || name == SECT98 || name == SECT99) { // prototype/proxy data = gData[name] } else if (scope+'_health_metrics' == name) { data = gData.health[scope+'_metrics'] target = 'health_metrics' } else if (zFP+'_metrics' == name) { data = gData[zFP][scope+'_metrics'] } else if (aShowFormat.includes(name)) { // FP/health if (isHealth) { if (!dom.healthAll.checked) {filter = dom.healthPass.checked ? '_pass' : '_fail'} if (overlayHealth == '_detail') {overlayHealth = ''; target = name} } else { if (overlayFP == '_detail') {overlayFP = ''; target = name} } data = gData[name][scope + (isHealth ? overlayHealth + filter : overlayFP)] } else if (name == 'alerts' || name == 'errors' || name == 'lies') { // global alerts/errors/lies data = gData[name][scope] } else if (isSection) { // section if (overlaySection == '_detail') {overlaySection = ''; target = name} data = sData[zFP][scope + overlaySection][name] } else { // section alerts/errors/lies let nameslice = name.slice(0,4) if (nameslice == 'erro' || nameslice == 'aler' || nameslice == 'lies') { let slicelen = nameslice == 'lies' ? 4 : 6 target = name.slice(0, slicelen) name = name.slice(slicelen) data = sData[target][scope][name] target = target.toUpperCase() +': '+ name } else { // detail data data = sDetail[scope][name] } } metricsData = data let aCached = ['fingerprint','fingerprint_flat','misc'] let isCache = aCached.includes(target) let cTarget = scope + ('misc' == target ? '_'+ target : overlayFP) let isColor = !(target == 'window_functions') let display = data !== undefined ? (isCache ? sDataTemp['cache'][cTarget] : json_highlight(data, isColor)): '' // dev: no need to display overlayInfo everywhere, limit if ('feature' == name) {dom.overlayInfo.innerHTML = overlayInfo} //add btn, show/hide options, display let hash = mini(data) metricsTitle = (scope == undefined ? '' : scope.toUpperCase() +': ') + target + filter +': '+ hash dom.metricsTitle.innerHTML = metricsTitle if (isVisible) { // avoid reflow dom.metricsDisplay.innerHTML = display } else { //dom.metricDownload.style.display = isShowFormat ? 'inline' : 'none' //^allow download everwhere dom.metricOptions.style.display = isShowFormat ? 'block' : 'none' dom.modaloverlay.style.display = 'block' dom.overlay.style.display = 'block' // delay so overlay is painted setTimeout(function() {dom.metricsDisplay.innerHTML = display}, 0) } } function metricsUI(target, isVisible, isSection, isHealth) { if (!isVisible) { // tidy up options dom.optFlat.style.display = target == 'fingerprint' ? 'block': 'none' dom.optList.style.display = target == 'fingerprint' ? 'block': 'none' dom.groupHealth.style.display = isHealth ? 'block' : 'none' // ensure suitable options let selected = '' if (isSection) { overlaySection = ('' == overlaySection ? '_detail' : '_summary') selected = overlaySection } else if (isHealth) { overlayHealth = ('' == overlayHealth ? '_detail' : '_summary') selected = overlayHealth } else { if (overlayFP == '') {overlayFP = '_detail'} selected = overlayFP } dom['optFormat'+ selected].checked = true } // update final target to match checked item let items = document.getElementsByName('optOverlay') for (let i=0; i < items.length; i++) { if (items[i].checked) { target += items[i].value let check = items[i].value if (isSection) {overlaySection = check} else if (isHealth) {overlayHealth = check} else {overlayFP = check} } } // return final target return target } /*** OUTGOING ***/ function lookup_health(sect, metric, scope, isPass) { // return summary 'error/untrustworthy/str/hash' + detail (underlying data) if ('window.caches' == metric) {metric = 'caches'} let data ='', hash ='' // error? try {data = gData['errors'][scope][sect][metric]; if (undefined !== data) {return([zErr, data])}} catch {} if ('pixels_match' == metric) { data = sDetail[scope][metric] if ('string' == typeof data) {return([zErr, data])} } // lie? try {data = gData['lies'][scope][sect][metric]; if (undefined !== data) {return([zLIE, zLIE])}} catch {} // nested, lookups, FP|detail data try { let nested ='', tmpdata, sDetailTemp if ('pixels_match' !== metric && 'pixels_' == metric.slice(0,7)) {nested = 'pixels'; metric = metric.replace('pixels_','')} if ('useragent_' == metric.slice(0,10)) {nested = 'useragent'; metric = metric.replace('useragent_','')} if ('media_' == metric.slice(0,6)) {nested = 'media'; metric = metric.replace('media_','')} if ('' !== nested) { data = gData[zFP][scope][sect]['metrics'][nested]['metrics'][metric] } else if (sDetail[scope].lookup[metric] !== undefined) { data = sDetail[scope].lookup[metric] } else if ('font_names' == metric || 'pixels_match' == metric) { // special case font names: not in FP / hash = full enumeration data = sDetail[scope][metric] hash = mini(data) } else { data = gData[zFP][scope][sect]['metrics'][metric] } if (undefined !== data) { let typeCheck = typeFn(data, true) hash = '' == hash ? data : hash // handle sDetailTemp: copy per run so it doesn't change in gData if (undefined !== sDetail[scope][metric]) { sDetailTemp = sDetail[scope][metric] if (!isPass) { // special case font names/faces: detail should reflect isPass: can't just check for !== undefined // e.g. windows FPP will still have unexpected data (for RFP) if ('font_faces' == metric || 'font_names' == metric || 'font_offscreen' == metric) { sDetailTemp = sDetail[scope][metric +'_health'] } } let tmpCheck = typeFn(sDetailTemp) if ('object' == tmpCheck) { data = {} for (const k of Object.keys(sDetailTemp)) {data[k] = sDetailTemp[k]} } else if ('array' == tmpCheck) { data = [] sDetailTemp.forEach(function(item){data.push(item)}) } } if ('object' === typeCheck) { try {hash = gData[zFP][scope][sect]['metrics'][metric].hash} catch {} } return([hash, data]) } } catch(e) { console.log(metric,e) } return(['','']) } function output_health(scope) { // done after populating global FP, errors, lies if (!isSmart) {return} let h = "health", countPass = 0, countTotal = Object.keys(gData.health[scope +'_collect']).length gData[h][scope] = {} gData[h][scope +'_metrics'] = [] gData[h][scope +'_fail'] = {} gData[h][scope +'_pass'] = {} gData[h][scope +'_summary'] = {} gData[h][scope +'_summary_fail'] = {} gData[h][scope +'_summary_pass'] = {} let target = gData.health[scope +'_collect'] try { for (const metric of Object.keys(target).sort()) { try { let sect = target[metric][0] let isPass = target[metric][1] if (isPass) {countPass++} let symbol = isPass ? tick : cross let sub = isPass ? '_pass' : '_fail' // lookup let data = lookup_health(sect, metric, scope, isPass) let summary = data[0], detail = data[1] if ('' !== summary) {summary = ' '+ summary} // populate detail if ('' == detail) {detail = symbol} gData[h][scope][metric] = detail gData[h][scope + sub][metric] = detail // populate summary + metriclist gData[h][scope +'_metrics'].push(metric) gData[h][scope +'_summary'][metric] = symbol + summary gData[h][scope +'_summary'+ sub][metric] = symbol + summary } catch(e) { console.log(metric, e) } } if (countTotal > 0) { let isAll = countPass == countTotal let overlayHealthCount = countPass +'/'+ countTotal let btnPart1 = addButton((isAll ? 'good' : 'bad'), h, countPass) btnPart1 = btnPart1.replace(']','') + '<span style="letter-spacing: -0.2em"> | </span>' dom[scope + h].innerHTML = btnPart1 + addButton(0,'document_health_metrics', countTotal).replace('[','') if (isAll) {dom.healthAll.checked = true} else {dom.healthFail.checked = true} } delete gData[h][scope +'_collect'] } catch(e) { console.log(e) } } function output_perf(id, click = false) { if (!isPerf) {return} let array = id == "all" ? gData["perf"] : sDataTemp["perf"] let target = id == "all" ? "perfG" : "perfS" let btn = id == "all" ? dom.perfGBtn : dom.perfSBtn // toggle perf depth let isMore = dom[target +"Btn"].innerHTML === "less" if (click) { isMore = !isMore // user toggled it dom[target +"Btn"] = isMore ? "less" : "more" } let aPretty = [], color1 = " <span class='s14'>", color2 = " <span class='s7'>", color3 = " <span class='s12'>", s98 = "<span class='s99'>", // trimmed pageLoad = false, isStart = false, nFix = isDecimal ? 2 : 0, pad = isDecimal ? 18 : 15, padFix = isDecimal ? 3 : 0 if (isMore) {pad = 32} try { array.forEach(function(array) { let type = array[0], name = array[1], time1 = array[2], time2 = array[3], extra = array[4] if ('string' == typeof extra) {extra = extra.trim()} extra = undefined == extra ? '' : extra !== '' ? ' | '+ extra : '' // header if (isMore && 1 === type) { if ("IMMEDIATE" == name) {pageLoad = true} if ("DOCUMENT START" == name) {isStart = true} time1 = pageLoad ? " "+ time1 : "" aPretty.push(color1 + name +":"+ time1 + sc) } // section/detail if (type > 1) { if (id == 'all') { time1 = time2 - time1 time2 = time2 - gt1 // use gt1: only reset on global runs } time1 = Number(time1).toFixed(nFix) time2 = Number(time2).toFixed(nFix) } // section if (2 === type) { time2 = id == "all" ? " |"+ color3 + time2.padStart(4 + padFix) +" ms</span>" : "" let pretty = name.padStart(pad) +":"+ color2 + time1.padStart(4 + padFix) +"</span> ms" + time2 + extra aPretty.push(pretty) if (sectionNames.includes(name)) { try { dom["perf"+ name] = " "+ time1 +" ms" } catch(e) { console.error('perf'+ name +' element is missing') } } } // detail if (isMore && 3 === type) { name = name.replace(": "+ SECTNF,"") if (name.length > pad) { let parts = name.split(":") let newlen = pad - (parts[1].length + 1) name = name.slice(0, newlen) +":"+ parts[1] } if (id !== "all") {isStart = true} time2 = isStart ? " |"+ s98 + time2.padStart(5 + padFix) +" ms</span>" : '' let pretty = s98 + name.padStart(pad) + sc +":" + s98 + time1.padStart(5 + padFix) +" ms</span>" + time2 + extra aPretty.push(pretty) } }) dom[target].innerHTML = aPretty.join("<br>") } catch(e) { console.error(e) } } function output_section(section, scope) { // ToDo: SANITY CHECKS let aSection = section == "all" ? sectionOrder : [section] // propagate onces to sDataTemp // don't worry about all vs section: sDataTemp is temp try { btnList.forEach(function(item) { let once = item+"once" if (gData[once] !== undefined && gData[once][scope] !== undefined) { for (const s of Object.keys(gData[once][scope])) { if (!sectionNames.includes(s)) { // non-section: straight to sData: sorted if (sData[item][scope] == undefined) {sData[item][scope] = {}} if (sData[item][scope][s] == undefined) {sData[item][scope][s] = {}} for (const m of Object.keys(gData[once][scope][s]).sort()) { sData[item][scope][s][m] = gData[once][scope][s][m] } } else { // section: to sDataTemp: no sort // this is just section onces: _all_ sections to sData etc are looped below if (sDataTemp[item][scope] == undefined) {sDataTemp[item][scope] = {}} if (sDataTemp[item][scope][s] == undefined) {sDataTemp[item][scope][s] = {}} for (const m of Object.keys(gData[once][scope][s])) { sDataTemp[item][scope][s][m] = gData[once][scope][s][m] } } } } }) } catch(e) { console.error(e) } // propagate data let gList = {}, gFlat = {} let summary = scope+'_summary', metriclist = scope+'_metrics' if (gRun) { gData[zFP][summary] = {} sData[zFP][summary] = {} gData[zFP][metriclist] = [] } aSection.forEach(function(number) { let data = {}, datasummary = {}, hash, count, hashsummary let name = sectionMap[number] sData[zFP][summary][name] = {} // section try { data = {}, datasummary = {} let obj = sDataTemp[zFP][scope][number] for (const k of Object.keys(obj).sort()) { data[k] = obj[k] let value = ("object" == typeof data[k] && data[k] !== null ? data[k]["hash"] : data[k]) datasummary[k] = value if (gRun) { gFlat[k] = obj[k] gList[k] = datasummary[k] } } sData[zFP][scope][name] = data hash = mini(data) count = Object.keys(data).length sData[zFP][summary][name] = datasummary hashsummary = mini(datasummary) // global if (gRun) { // FP gData[zFP][scope][name] = {} gData[zFP][scope][name]["hash"] = hash gData[zFP][scope][name]["metrics"] = data // summary gData[zFP][summary][name] = {} gData[zFP][summary][name]["hash"] = hashsummary gData[zFP][summary][name]["metrics"] = datasummary } } catch(e) { console.error(e) } // display try { for (const d of Object.keys(sDataTemp["display"][scope][number])) { let str try { str = sDataTemp["display"][scope][number][d] dom[d].innerHTML = str } catch(e) { console.error(d, str, e.name, e.emssage) } } } catch(e) { console.error(e) } let sHash = hash + addButton(0, name, count +" metric"+ (count == 1 ? "" : "s"), "btns") // section buttons // sDataTemp sorted to sData let aBtns = [] try { btnList.forEach(function(item) { let btn ='' if (sDataTemp[item][scope] !== undefined && sDataTemp[item][scope][name] !== undefined) { if (sData[item][scope] == undefined) {sData[item][scope] = {}} if (sData[item][scope][name] == undefined) {sData[item][scope][name] = {}} let typeCheck = typeFn(sDataTemp[item][scope][name], true) for (const m of Object.keys(sDataTemp[item][scope][name]).sort()) { sData[item][scope][name][m] = sDataTemp[item][scope][name][m] } let count = Object.keys(sData[item][scope][name]).length if (count > 0) { let btnText = count + ' '+ (count == 1 ? item.slice(0,-1) : item) // single/plural let color = ('alerts' === item) ? 'bad' : 0 btn = addButton(color, item + name, btnText, 'btns', scope) aBtns.push(btn.trim()) } } }) dom[name +'hash'].innerHTML = sHash + aBtns.join('') } catch(e) { console.error(e) } }) if (gRun) { // flat + list let flat = scope+'_flat', list = scope+'_list' gData[zFP][flat] = {} gData[zFP][list] = {} for (const k of Object.keys(gFlat).sort()) { gData[zFP][flat][k] = gFlat[k] gData[zFP][metriclist].push(k) // metric list } for (const k of Object.keys(gList).sort()) {gData[zFP][list][k] = gList[k]} // recalculate overlayCharLen for cached big jsons try { // at a minimum this is 10 chars "0, 0, 0, 0" all platforms/engines let target = dom.position_screen overlayCharLen = target.offsetWidth/target.innerText.length } catch(e) { overlayCharLen = 7 console.error(e) } // cache big json displays sDataTemp['cache'] = {} sDataTemp['cache'][scope] = json_highlight(gData[zFP][scope]) sDataTemp['cache'][flat] = json_highlight(gData[zFP][flat]) } if (gRun || 18 == section) { // cache misc sDataTemp['cache'][scope +'_misc'] = json_highlight(gData[zFP][scope]['misc']) } } /*** RECORD ***/ function addButton(color, name, text = 'details', btn = 'btnc', scope = isScope) { if ('' == text) {text = 'details'} return " <span class='btn"+ color +' '+ btn +"' onClick='metricsShow(`"+ name +"`,`" + scope +"`)'>["+ text +"]</span>" } function addBoth(section, metric, str, btn ='', notation ='', data ='', isLies = false, donotuse ='x') { //if ('x' !== donotuse) {console.log(metric, 'extra paramater passed')} if (undefined == str) {str += ''} let display = str // check: errors can't be lies if (data == zErr || data == zErrLog || data == zErrShort) { isLies = false let sectionName = sectionMap[section] // instead of logging errors in each function add them here if (data == zErrLog || data == zErrShort) { // 186 errors: 82e9678e (runST + runSI) // 190 errors: 4440a20a (+ runSE) display = log_error(section, metric, str) if (data == zErrShort) {display = zErr} data = zErr } } // check: non obj can't have btns if ('object' !== typeof data && '' !== btn) { let typeCheck = typeFn(data, true), value if ('object' !== typeCheck && 'array' !== typeCheck) {btn =''} } addDisplay(section, metric, display, btn, notation, isLies) // data = FP: if missing we use str/ which also doubles as our hash for objects if ('' == data || undefined == data) {data = str} addData(section, metric, data, str, isLies) } function addData(section, metric, data, hash ='', isLies = false, donotuse ='x') { //if ('x' !== donotuse) {console.log(metric, 'extra paramater passed')} // check: basic mode/errors can't be lies if (!isSmart || data == zErr) {isLies = false} let typeCheck = typeFn(data, true), value if ('object' === typeCheck || 'array' === typeCheck) { addDetail(metric, data) value = {'hash': hash, 'metrics': data} } else { value = data } sDataTemp[zFP][isScope][section][metric] = isLies ? zLIE : value if (isLies) { // don't add spoofed domrect data let aIgnore = ['element_font','element_forms','element_mathml','element_other','glyphs'] if (aIgnore.includes(metric)) {value = zLIE} log_known(section, metric, value) } } function addDisplay(section, metric, str ='', btn ='', notation ='', isLies = false, donotuse ='x') { //if ('x' !== donotuse) {console.log(metric, 'extra paramater passed')} // check: basic mode can't be lies or have notation if (!isSmart) {isLies = false; notation =''} // style lies + ensure lies cannot be good health if (isLies) { str = "<span class='lies'>"+ str +"</span>" notation = notation.replace("class='good'", "class='bad'") notation = notation.replace(tick, cross) } str += '' if (isSmart && !isLies && 8 == str.length && !str.includes(' ')) { // limit to hopefully just hashes if (str.includes('6') && str.includes('7')) { if (str.indexOf('6') < str.indexOf('7')) { str = str.replace('7','<span class="s67 s' + section +'">7</span>') str = str.replace('6','<span class="s67 s' + section +'">6</span>') } } } if ('manual' == isScope) { sDataTemp['display'][isScope][metric] = str + btn + notation return } sDataTemp['display'][isScope][section][metric] = str + btn + notation // global health: just grab pass/fail if (gRun && '' !== notation && notation.includes("class='health'")) { let isPass if (notation.includes('>'+ tick +'<')) {isPass = true} else if (notation.includes('>'+ cross +'<')) {isPass = false} if (isPass !== undefined) { gData['health'][isScope +'_collect'][metric] = [sectionMap[section], isPass] } } } function addDetail(metric, data, scope = isScope) { if (sDetail[scope] == undefined) {sDetail[scope] = {}} sDetail[scope][metric] = data if (gRun) {addTiming(metric)} } function addTiming(metric) { let remainder = gCountTiming % 8, key, value if (0 == gCountTiming % 5) { // get extra dates try {gData.timing['date'].push((new Date())[Symbol.toPrimitive]('number'))} catch {} } try { if (0 == remainder) { key = 'exslt' if (!isXSLT) {throw zSKIP} const xslText = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"' +' xmlns:date="http://exslt.org/dates-and-times" extension-element-prefixes="date"><xsl:output method="html"/>' +' <xsl:template match="/"><xsl:value-of select="date:date-time()" /></xsl:template></xsl:stylesheet>' const doc = (new DOMParser).parseFromString(xslText, "text/xml") let xsltProcessor = new XSLTProcessor xsltProcessor.importStylesheet(doc) let fragment = xsltProcessor.transformToFragment(doc, document) value = (fragment.childNodes[0].nodeValue).slice(0,-6) } else if (1 == remainder || 5 == remainder) { key = 'now'; value = performance.now() } else if (2 == remainder) { key = 'currenttime'; value = gTimeline.currentTime } else if (3 == remainder) { key = 'timestamp'; value = new Event('').timeStamp } else if (4 == remainder) { key = 'date' value = (new Date())[Symbol.toPrimitive]('number') } else if (6 == remainder) { performance.mark('a') } else if (7 == remainder) { key = 'instant' value = Temporal.Now.instant().toString() } if (undefined !== key) { if (runST) {value = undefined} gData.timing[key].push(value) } } catch(e) { gData.timing[key] = e+'' } gCountTiming++ } function addTimings() { // get first and final values for each to ensure a max diff try {gData.timing['now'].push(performance.now())} catch {} try {gData.timing['timestamp'].push(new Event('').timeStamp)} catch {} try {gData.timing['date'].push((new Date())[Symbol.toPrimitive]('number'))} catch {} try {gData.timing['instant'].push(Temporal.Now.instant().toString())} catch {} try { if (0 == gCountTiming) {gTimeline = new DocumentTimeline()} gData.timing['currenttime'].push(gTimeline.currentTime) } catch {} try { if (0 == gCountTiming) {performance.clearMarks('a')} performance.mark('a') } catch {} if (0 == gCountTiming) { addTiming('start') // adds first exslt } } function log_alert(section, metric, alert, scope = isScope, isOnce = false) { if ('string' !== typeof section) {section = sectionMap[section]} let key = 'alerts' if (gRun && isOnce) { key += 'once' //if (gData[key][scope] == undefined) {gData[key][scope] = {}} if (gData[key][scope][section] == undefined) {gData[key][scope][section] = {}} gData[key][scope][section][metric] = alert } else { if (sDataTemp[key][scope] == undefined) {sDataTemp[key][scope] = {}} if (sDataTemp[key][scope][section] == undefined) {sDataTemp[key][scope][section] = {}} sDataTemp[key][scope][section][metric] = alert } } function log_error(section, metric, error = zErr, scope = isScope, isOnce = false) { if ('string' !== typeof section) {section = sectionMap[section]} if ('' == error || null == error || undefined == error) {error = zErr} else {error += ''} let aLen25 = [ 'canPlayType','isTypeSuppo','font-format','font-tech','textmetrics', ] let len = isDesktop ? 50 : 25 if (aLen25.includes(metric.slice(0,11))) {len = 25} let key = 'errors' // collect if (gRun && isOnce) { key += 'once' if (gData[key][scope] == undefined) {gData[key][scope] = {}} if (gData[key][scope][section] == undefined) {gData[key][scope][section] = {}} gData[key][scope][section][metric] = error } else { if (sDataTemp[key] == undefined) {sDataTemp[key] = {}} if (sDataTemp[key][scope] == undefined) {sDataTemp[key][scope] = {}} if (sDataTemp[key][scope][section] == undefined) {sDataTemp[key][scope][section] = {}} sDataTemp[key][scope][section][metric] = error } // trim if required + return // is aLen25 and android, just display zErr if (!isDesktop && aLen25.includes(metric.slice(0,11))) { error = zErr } else if (error.length > len) { error = error.slice(0,len-3) + "..." } return error } function log_known(section, metric, data, scope = isScope) { if (!isSmart) {return data} let key = 'lies' if ('string' !== typeof section) {section = sectionMap[section]} if (sDataTemp[key][scope] == undefined) {sDataTemp[key][scope] = {}} if (sDataTemp[key][scope][section] == undefined) {sDataTemp[key][scope][section] = {}} if (undefined == data) { if (undefined !== sDetail[scope][metric]) {data = sDetail[scope][metric]} } sDataTemp[key][scope][section][metric] = data // color return "<span class='lies'>"+ data +"</span>" } function log_perf(section, metric ='', time1, time2, extra) { if (!isPerf) {return} if ('string' !== typeof section) {section = sectionMap[section]} let tEnd = performance.now() let str = '' === metric ? section : metric +': '+ section // GLOBAL if (gRun || str.includes(SECTNF)) { gData.perf.push([3, str, time1, tEnd, extra]) return } // SECTION RERUNS if (str.includes(SECTP)) {return} // ignore prereq let type = sectionNames.includes(str) ? 2 : 3 time2 = tEnd - gt0 time1 = (2 === type) ? time2 : tEnd - time1 sDataTemp.perf.push([type, str, time1, time2, extra]) } function log_section(name, time, scope = isScope) { let t0 = nowFn() let nameStr = "number" === typeof name ? sectionMap[name] : name if (gRun) {gData["perf"].push([2, nameStr, time, t0])} if (nameStr == SECTP) {return} //console.log(name, nameStr) // SECTION RERUNS if (!gRun) { log_perf(nameStr,'', time) output_section(name, scope) outputPostSection(name) // trigger nonFP gClick = true return } // GLOBAL gCount++ //console.log(sectionMap[name], gCount ,"/", gSectionsExpected) if (gCount == gSectionsExpected) { // NoScript can be slow - check quirks mode at the end - we only need do this once if (gLoad) { try {if ('CSS1Compat' !== document.compatMode) {run_block('quirks'); return}} catch(e) {} } gt1 = gt0 if (isPerf) {dom.perfAll = " "+ (performance.now()-gt0).toFixed(isDecimal ? 2 : 0) +" ms"} output_section("all", scope) // FP try { let metricCount = Object.keys(gData[zFP][scope +"_flat"]).length let color = (metricCount == expectedMetrics || sectionIgnore.length) ? 0 : 'red' // use red to override color in basic mode let btnPart1 = addButton(color, zFP, metricCount) btnPart1 = btnPart1.replace(']','') + '<span style="letter-spacing: -0.2em"> </span>' dom[scope + 'hash'].innerHTML = mini(gData[zFP][scope]) + btnPart1 + addButton(0,'fingerprint_metrics', 'metrics').replace('[','') } catch(e) { console.log(e) } let aBtns = [] try { btnList.forEach(function(item) { let total = 0, oFlat = {}, oSummary = {} // propagate sData to gData if (sData[item][scope] !== undefined) { if (gData[item][scope] == undefined) { gData[item][scope] = {} } for (const s of Object.keys(sData[item][scope]).sort()) { // everything is already sorted gData[item][scope][s] = sData[item][scope][s] total += Object.keys(sData[item][scope][s]).length for (const k of Object.keys(sData[item][scope][s])) { let tmpData = sData[item][scope][s][k] let value = ('object' == typeof tmpData && tmpData !== null ? sData[item][scope][s][k]['hash'] : sData[item][scope][s][k]) } } } if (total > 0) { let btnText = total +" "+ (total == 1 ? item.slice(0,-1) : item) // single/plural let color = ('alerts' === item) ? 'bad' : 0 aBtns.push(addButton(color, item, btnText, 'btnc', scope)) } }) } catch(e) { console.error(e) } dom[scope +"btns"].innerHTML = aBtns.join("") // prototype/proxy // ToDo: isTB health let protoDisplay = zNA if (isProtoProxy) { protoDisplay = 'none' let propsCount = gData[SECT97].length let protoCount = (Object.keys(gData[SECT98]).length) let proxyCount = gData[SECT99].length if (protoCount + proxyCount > 0) { let aStr = [] if (protoCount > 0) {aStr.push(mini(gData[SECT98]) + addButton(0, SECT98, protoCount))} if (proxyCount > 0) {aStr.push(mini(gData[SECT99]) + addButton(0, SECT99, proxyCount))} protoDisplay = aStr.join(" ") } protoDisplay += ' from '+ addButton(0, SECT97, propsCount) } dom.protohash.innerHTML = protoDisplay output_health(scope) // trigger nonFP outputPostSection("all") gLoad = false gClick = true } } /*** RUN ***/ function countJS(item) { jsFiles++ if (1 == jsFiles) { // block quirks mode e.g. caused by NoScript try {if ('CSS1Compat' !== document.compatMode) {run_block('quirks'); return}} catch(e) {} // block if iframed if (window.location !== window.parent.location) {run_block('iframe'); return} // block if insecure as this produces very different results e.g. some APIs require secure // gecko diffs include 14 navigator keys, 100+ window props, and 7 permissions if (!isFile && 'https:' !== location.protocol) {run_block('insecure'); return} // non-gecko if (!isGecko) { if (isEngineBlocked) {run_block('upgrade'); return} if (isAllowNonGecko && undefined !== isEngine) {run_basic()} else {run_block(isEngine+' engine'); return} } // update tooltip if (undefined !== isStylesheet) { try { let items = document.getElementsByClassName('cssrange') for (let i=0; i < items.length; i++) {items[i].innerHTML = 'range '+ isStylesheet} } catch(e) {} } // set src's for our l10n iframe tests // setting these inline can cause the wrong contentDocument in the wrong iframes // it's almost random like some sort of race with different results in android vs windows - WTF!! // Switching the element order in html (with inline src and not by JS) I can replicate these // mismatched contents in both windows and android. Only setting them via JS are we always correct // jesus says: WT actual F if (isGecko) { try {dom.tzpInvalidImage.src = 'images/InvalidImage.png'} catch {} try {dom.tzpScaledImage.src = 'images/ScaledImage.png'} catch {} try {dom.tzpXMLunstyled.src = 'xml/xmlunstyled.xml'} catch {} try {dom.tzpXSLT.src='xml/xslterror.xml'} catch {} // in FF134 or lower this breaks devtools: oh dear, what a shame } get_isVer('isVer') // if PoCs don't touch the dom this is fine here: required for isTB get_isSystemFont() return } else if (jsFiles === jsFilesExpected) { // block: quirks, iframe, insecure, upgrade required, !isAllowNonGecko, undefined isEngine | also if gecko is below min version if (isStop) {return} // otherwise not blocked isBlock = false // tidy up metric overlay symbols to match global symbol used dom.overlay_tick.innerHTML = tick +' ' dom.overlay_cross.innerHTML = cross +' ' gData['perf'].push([1, 'RUN ONCE', nowFn()]) Promise.all([ get_isBB('isBB'), get_isBrave('isBrave'), get_isFileSystem('isFileSystem'), get_isAutoplay('getAutoplayPolicy'), ]).then(function(){ // 140+ notations: if isBB then block FPP notations & vice versa isFPPFallback = !isBB Promise.all([ get_isOS('isOS') ]).then(function(){ // tweak monospace size // ToDo: this is bad design: we need a better way to get nice consistent sizes across // TB vs linux vs other linux vs other platforms /* if ('windows' == isOS) { try { document.body.style.setProperty('--txtSize', '12px') document.body.style.setProperty('--txtSizeBigger', '24px') } catch(e) {console.log(e)} } //*/ // do once dom.tzpPointer.addEventListener('pointerdown', (event) => {outputUser('pointer_event', event)}) dom.tzpPointer.addEventListener('pointerrawupdate', (event) => {get_isPointerRawUpdate(event)}) if (isDesktop) { document.addEventListener('keydown', metricsEvent) } else { showhide('A','table-row') // A1 inner_document: html class hidden - only used by android // add class togS so it shows when expanding, remove hidden class dom.A1.classList.add('togS') dom.A1.classList.remove('hidden') // hide and remove togS on the entire viewport section + also window.inner - not used by android // + visualViewportScale + window_scrollbar let items = document.getElementsByClassName('A2') for (let i=0; i < items.length; i++) { items[i].classList.remove('togS') items[i].classList.add('hidden') } // hide console button in overlay: width is a premium dom.metricsConsole.classList.add('hidden') } // set isBBESR: some health checks we only want to do if it's worthwhile // android and alpha are moving to RR and it's not ffeasible to keep up with per release changes if (isBB && 'android' !== isOS && isVer == 140) {isBBESR = true} Promise.all([ get_isFontDelay() // determine if we need to delay BB for font.vis and async font fallback ]).then(function(){ outputSection('load') }) }) }) } } function outputPostSection(id) { if ("all" !== id) { output_perf(id) } if ("number" === typeof id) {id = sectionMap[id]} if (gRun) {gData["perf"].push([1, SECTNF, nowFn()])} let isLog = gRun // push perf gRun = false // stop collecting if (id == "storage") { test_worker(isLog) test_worker_service(isLog) // doesn't return test_worker_shared(isLog) test_idb(isLog) } else if (id == "agent") { get_agent_iframes(isLog) get_agent_workers() } else if (id == "all") { test_worker_service(isLog) // doesn't return Promise.all([ test_worker(isLog), test_worker_shared(isLog), test_idb(isLog), get_agent_iframes(isLog), get_agent_workers(), ]).then(function(){ output_perf(id) }) } } function outputSection(id, isResize = false) { if (isBlock || !gClick) { output_perf('all') return } // set the onion skin pattern background if TB and dark if (gLoad && isTB && 'android' !== isOS) { try { let target = document.body let bgcolor = window.getComputedStyle(target).getPropertyValue('background-color') if ('rgb(22, 27, 34)' == bgcolor) { target.classList.add('tzpBody') } else { target.classList.remove('tzpBody') } } catch(e) {} } // reset scope isScope = zDOC if ('load' == id) { // set sectionOrder/Names/Nos let tmpObj = {} for (const k of Object.keys(sectionMap)) {sectionNos[sectionMap[k]] = k; tmpObj[sectionMap[k]] = k; sectionNames.push(sectionMap[k])} for (const n of Object.keys(tmpObj).sort()) {sectionOrder.push(tmpObj[n])} sectionNames.sort() } gClick = false let delay = 100 // reset if ('load' == id || 'all' == id) { // show hide sections let sState = mini(sectionIgnore) if (sState !== sectionState) { console.log('hiding these sections', sectionIgnore) sState = sectionState for (const n of Object.keys(sectionNos)) { if (undefined !== sectionNos[n]) { let tbltarget = dom['tb'+ sectionNos[n]] if (sectionIgnore.includes(n)) { tbltarget.classList.add('hidden') } else { tbltarget.classList.remove('hidden') } } } } // gData if (runSG) { log_error('a', 'd', '4', isScope, true) log_error('_a', 'a', '1', isScope, true) log_error(2, 'z', '9', isScope, true) log_error(2, 'y', '8', isScope, true) log_error(SECTG, 'c', '3', isScope, true) log_error(SECTG, 'b', '2', isScope, true) log_alert(5, 'z', "p", isScope, true) log_alert(5, '_a', "t", isScope, true) log_alert(5, 'm', "z", isScope, true) } gData[zFP] = {'document': {}} gData.health = {'document_collect': {}} gTiming.forEach(function(item){gData.timing[item] = []}) btnList.forEach(function(item){gData[item] = {}}) if (!gLoad) { // don't wipe gLoad perf gData['perf'] = [] } // sData sData = { 'fingerprint': {'document': {}} } // sDataTemp sDataTemp = { 'display': {'document': {}}, 'fingerprint': {"document": {}}, 'perf': [], } btnList.forEach(function(item){ sData[item] = {'document': {}} sDataTemp[item] = {'document': {}} }) for (const name of Object.keys(sectionMap)) { let sectionName = sectionMap[name] sDataTemp[zFP][isScope][name] = {} sDataTemp['display'][isScope][name] = {} } // sDetail sDetail = {'document': {'lookup': {}}, 'manual': {}} } if ('load' == id) { // skip clear/reset id = 'all' delay = 0 } else if ('all' == id) { gRun = true // clear let items = document.getElementsByClassName('c') for (let i=0; i < items.length; i++) {items[i].innerHTML = '&nbsp'} items = document.getElementsByClassName('cssc') // inline css notations we don't want to add an empty space for (let i=0; i < items.length; i++) {items[i].innerHTML = ''} items = document.getElementsByClassName('gc') // user actions for (let i=0; i < items.length; i++) {items[i].innerHTML = '&nbsp'} // reset global gCount = 0 get_isDevices() // non gLoad warmup } else { // clear section data let name = sectionMap[id] try {sData[zFP][isScope][name] = {}} catch {} try {sDataTemp[zFP][isScope][id] = {}} catch {} try {sDataTemp['display'][isScope][id] = {}} catch {} btnList.forEach(function(item){ try {sData[item][isScope][name] = {}} catch {} try {sDataTemp[item][isScope][name] = {}} catch {} }) if (!isResize) { let tbl = dom['tb'+ id] tbl.querySelectorAll(`.c`).forEach(e => {e.innerHTML = '&nbsp'}) tbl.querySelectorAll('span.cssc').forEach(e => {e.innerHTML = ''}) } gRun = false } var promiseSection = async function(x) { let n = Number.isInteger(x) ? x : sectionNos[x] if (n == 1) { return(outputScreen(isResize))} if (n == 2) { return(outputAgent())} if (n == 3) { return(outputFD())} if (n == 4) { return(outputRegion())} if (n == 5) { return(outputHeaders())} if (n == 6) { return(outputStorage())} if (n == 7) { return(outputDevices())} if (n == 9) { return(outputCanvas())} if (n == 10) { return(outputWebGL())} if (n == 11) { return(outputAudio())} if (n == 12) { return(outputFonts())} if (n == 13) { return(outputMedia())} if (n == 14) { return(outputCSS())} if (n == 15) { return(outputElements())} if (n == 17) { return(outputTiming())} if (n == 18) { return(outputMisc())} } function output() { if ('all' == id) { if (!gLoad) { gCountTiming = 0 // reset addTimings() } // run sequentially awaiting each before running the next // order: use number or section name let order = [ 3, // first: sets isMB (legacy method) 2, 1, 5, 14, 13, // fast 'canvas', 'storage', // little slow: cache + permissions 'misc', // cold on load: iframe props 'elements', // cold on load: mathml 'audio', 'webgl', 'devices','fonts', // near last: allow time for isDevices, font fallback 'region', // next to last: allow time for iframes and any reporting API items to manifest on first run 17 // last: uses data collected during gRun ] const forEachSection = async (iterable, action) => { for (const n of iterable) { let t0 = nowFn() await promiseSection(n) let x = Number.isInteger(n) ? n : sectionNos[n] * 1 log_section(x, t0) } } forEachSection(order, promiseSection) } else { gt0 = nowFn() // single section timer Promise.all([ promiseSection(id) ]).then(function(){ log_section(id, gt0, isScope, isResize) }) } } // set isXSLT which we need before we start any timing measurements get_isXSLT() // reset smarts smartFn('final') let enforcedDelay = 0 if (gLoad) { // initiate timings gCountTiming = 0 addTimings() // force an initial delay regardless | moreso if it it's BB with font.vis // e.g. some extensions can be slow to inject etc // e.g. will help with resources such as XML/images and enforcedDelay = isFontDelay ? 3000 : (isFile ? 0 : 1200) } //if (gLoad) {enforcedDelay = 1200} if (enforcedDelay > 0) { delay = 0 let msg = isFontDelay ? 'async font fallback' : '' let padLength = (enforcedDelay+'').length if (isFontDelay) {dom.protohash.innerHTML = '<span class="spaces"> awaiting '+ msg} let t0 = nowFn(), increment = 16 // 5 is too fast to read function countdown() { addTiming() // add loads of various timing measurements during our delay let timetaken = Math.floor(nowFn() - t0) if (timetaken > enforcedDelay) { dom.protohash.innerHTML = '' clearInterval(timer) log_perf(SECTG, 'enforced delay', t0,'', msg) proceed() } else { let remainder = enforcedDelay - timetaken if (remainder > (increment * 2)) { remainder ='<span class="spaces"> running in ... ' + (remainder+'').padStart(padLength) +' ms</span>' } else (remainder = '') dom.documenthash.innerHTML = remainder } } let timer = setInterval(countdown, increment) } else { proceed() } function proceed() { setTimeout(function() { get_isPerf() if (gRun) {gData['perf'].push([1, 'DOCUMENT START', nowFn()])} gt0 = nowFn() Promise.all([ get_isDomRect(), outputPrototypeLies(isResize), ]).then(function(){ if (isBB && gClear && 'all' == id) {console.clear()} if (isSmart) {log_section(SECTP, gt0)} // WTF NoScript! sometimes we have to catch this later try {if ('CSS1Compat' !== document.compatMode) {run_block('quirks'); return}} catch(e) {} output() }) }, delay) } } function run_immediate() { get_isPerf() let t00 = nowFn() zErrLog = rnd_string() zErrShort = rnd_string() gData['perf'].push([1, 'IMMEDIATE', t00]) isFile = 'file:' == location.protocol Promise.all([ get_isGecko('isGecko') ]).then(function(){ if (!isGecko) { // get engine regardless get_isEngine('isEngine') // return if not supported if (!isAllowNonGecko || undefined === isEngine) {return} } else { // search https://searchfox.org/firefox-main/source/dom/locales/en-US/chrome/dom/dom.properties for 'is deprecated' // trigger some gecko deprecated items. They are recorded regardless of API. The API pref simply allows access to read them let aItems = [ 'InstallTrigger', // X is deprecated and will be removed in the future. | FF100 1754441 'fullScreen', // X is deprecated. Use Y instead. | FF65 1504946 'onmozfullscreenchange', 'onmozfullscreenerror' // X is deprecated ] aItems.forEach(function(n) {try {window[n]} catch(e) {}}) try {document.releaseCapture()} catch(e) {} // FF90 973604 mark set/releaseCapture() as deprecated // X is deprecated. Use Y instead. For more help try {screen.mozOrientation} catch(e) {} // FF147 2003169 deprecate mozOrientation // X attribute is deprecated and will be removed in the future. } // set isProtoProxy on known engines // we already returned if isEngine == undefined just above isProtoProxy = 'undefined' == isEngine ? false : true // expand css, record stylesheet info get_isStylesheet(7680) // recursion get_isRecursion() // storage warm ups get_isFileSystem('isFileSystem', true) try {window.caches.keys()} catch {} // other warm ups get_isDevices() try {let w = speechSynthesis.getVoices()} catch {} try { const config = {initDataTypes: ['cenc'], videoCapabilities: [{contentType: 'video/mp4;codecs="avc1.4D401E"'}]} navigator.requestMediaKeySystemAccess('org.w3.clearkey', [config]).then((key) => {}).catch(function(e){}) } catch {} try { let warm = Intl.DateTimeFormat().resolvedOptions() warm = Intl.DateTimeFormat(undefined, {timeZone: 'Europe/London', timeZoneName: 'shortGeneric'}).format(new Date) warm = new Intl.NumberFormat(undefined, {notation: 'compact'}).format(1) warm = new Intl.NumberFormat(undefined, {style: 'unit', unit: 'hectare'}).format(1) } catch {} get_isXML() get_isArch('isArch') try { // ensure hevc correctness e.g. see 1972902 fixed by 1974881 // 1st query on a new session hevc are false positives: a recheck fixes it let vCodecs = ['"hev1.1.6.L93.B0"','"hev1.2.4.L120.B0"','"hvc1.1.6.L93.B0"','"hvc1.2.4.L120.B0"'] let vObj = document.createElement('video') vCodecs.forEach(function(item) {let vTest = vObj.canPlayType('video/mp4; codecs='+ item)}) } catch(e) {} }) } run_immediate() ================================================ FILE: js/globals.js ================================================ 'use strict'; var dom; const SECTG = '_global', SECTP = '_prereq', SECTNF = 'NON-FP', SECT97 = 'properties', SECT98 = 'prototype', SECT99 = 'proxy' const sectionMap = { 1: 'screen', 2: 'agent', 3: 'feature', 4: 'region', 5: 'headers', 6: 'storage', 7: 'devices', 9: 'canvas', 10: 'webgl', 11: 'audio', 12: 'fonts', 13: 'codecs', 14: 'css', 15: 'elements', 17: 'timing', 18: 'misc', } let sectionOrder = [], // numerical order for objects sectionNames = [], // lookup names by number sectionNos = {}, // lookup numbers by name sectionIgnore = [], // show/hide on dom't run if gRun sectionState = 'ac6c4be7' // default empty array: sectionIgnore hash so we know to trigger show/hide // ToDo: expand: some info can go into lies but we could create new items e.g methods/tampered-data // some 'methods/entropy' are in the FP: e.g. canvas/domrect or errors e.g. font sizes const btnList = ['alerts', 'errors', 'lies'] const jsFilesExpected = 15, gSectionsExpected = 16, expectedMetrics = 135 let jsFiles = 0, gCount = 0, gCountTiming = 0 // global let gData = { // from sData 'alertsonce': {'document': {}}, 'errorsonce': {'document': {}}, 'health': {'document': {}}, 'perf': [], 'timing': {}, } let gTiming = ['currenttime','date','exslt','instant','mark','navigation','now','performance','reducetimer','resource','timestamp'] let gTimeline // section let sData = {}, // final sorted section data: from sDataTemp sDataTemp = {}, // unsorted section data sDetail = {} // all clickables: lies, fake, valid etc // scopes const zFP = 'fingerprint', zDOC = 'document', zIFRAME = 'iframe' let isScope = zDOC // styles let s0 = " <span class='", sb = s0+"bad'>", sg = s0+"good'>", s1 = s0+"s1'>", // s1+s3+s99: used in perf details s3 = s0+"s3'>", s99 = s0+"s99'>", sc = '</span>' // common const zD = 'disabled', zE = 'enabled', zErr = 'error', zErrType = 'TypeError: ', zErrTime = 'timed out', zErrInvalid = 'Invalid: ', zNA = 'n/a', zS = 'success', zF = 'failed', zNEW = sb+'[NEW]'+sc, zLIE = 'untrustworthy', zSKIP = 'skipped' let zErrLog = '', // log error in add/Both zErrShort = '' // log error in add/Both but display zErr in add/Display // grab as soon as possible let isInitial = {height: {}, width: {}} function get_scr_initial() { // we don't need any error entropy: we get these properties again later let x, oList = {height: ['innerHeight','outerHeight'], width: ['innerWidth','outerWidth']} for (const axis of Object.keys(oList)) { let aList = oList[axis] aList.forEach(function(k){ try { x = window[k] if ('number' !== typeof x || Number.isNaN(x)) {x = zErr} } catch { x = zErr } if (k.includes('inner')) {k = 'inner'} else {k = 'outer'} isInitial[axis][k] = x }) } } get_scr_initial() // notation // https://en.wikipedia.org/wiki/Check_mark // https://en.wikipedia.org/wiki/X_mark const tick = '✓', // ✓ u2713, 🗸 u1F5F8 cross = '✗', // ✗ u2717, 🗴 u!F5F4, 🞩 u1F7A9 green_tick = sg+"<span class='health'>"+ tick +'</span>'+sc, green_benign = sg+"[<span class='health'>"+ tick +'</span> benign]'+sc, red_cross = sb+"<span class='health'>"+ cross +'</span>'+ sc, red_benign = sb+"[<span class='health'>"+ cross +'</span> benign]'+ sc, sgtick = sg +"[<span class='health'>"+ tick +'</span> ', sbx = sb +"[<span class='health'>" + cross +'</span> ', rfp_green = sgtick+'RFP]'+sc, rfp_red = sbx+'RFP]'+sc, silent_green = sg +"[<span class='healthsilent'>"+ tick +'</span>]'+ sc, silent_red = sb +"[<span class='healthsilent'>" + cross +'</span>]'+ sc, silent_rfp_green = sg +"[<span class='healthsilent'>"+ tick +'</span> RFP]'+ sc, silent_rfp_red = sb +"[<span class='healthsilent'>" + cross +'</span> RFP]'+ sc, nw_green = sgtick+'RFP newwin]'+sc, nw_red = sbx+'RFP newwin]'+sc, default_green = sgtick+'default]'+sc, default_red = sbx+'default]'+sc, match_green = sgtick+'match]'+sc, match_red = sbx+'match]'+sc, fpp_green = sgtick+'FPP]'+sc, lang_green = sgtick+' languages]'+sc, lang_red = sbx+' languages]'+sc, locale_green = sgtick+' locale]'+sc, locale_red = sbx+' locale]'+sc, localetz_green = sgtick+' locale + timezone]'+sc, localetz_red = sbx+' locale + timezone]'+sc, intl_green = sgtick+' intl]'+sc, intl_red = sbx+' intl]'+sc, tz_green = sgtick+' timezone]'+sc, tz_red = sbx+' timezone]'+sc, desktopmode_green = sgtick+'RFP desktop mode]'+sc // dynamic BB notation let bb_green = sgtick+'TB]'+sc, bb_red = sbx+'TB]'+sc, bb_slider_red = sbx+'TB Slider]'+sc, bb_standard = sg+'[TB Standard]'+sc, bb_safer = sg+'[TB Safer]'+sc // don't tick/cross slider // run once let isArch = true, isAutoPlay, isAutoPlayError, isBrave = false, isBraveSmart = false, // only if FP protection is on isDesktop = true, isDevices, isEngine, isEngineBlocked = true, isEngineStr = '', isFile = false, isFileSystem, isFileSystemError, isFontDelay = false, // BB win/mac require a delay for async font fallback if font.vis used isGecko = false, isOS, isOSErr, isProps, // window properties isProtoProxy = false, isRecursion, isReporting, // ReportingAPI isScrollbar, isStyles = ['cursive','math','monospace','sans-serif','serif','system-ui'], // FF145+ nightly 1788937 math | 2014703 FF149+ // 'emoji','ui-monospace','ui-rounded','ui-serif' = currently at least gecko + blink redundant (windows) // 'emoji' = better covered in special metric/test targeting emojis/unicode // 'fantasy' = not set in gecko (checked Feb 2026) see 536004#c2 isStylesAll = [ 'cursive','emoji','fangsong','fantasy','math','monospace', 'sans-serif','serif','system-ui','ui-monospace','ui-rounded','ui-serif' ], isStylesheet, isSystemFont = [], isVer = 0, isVerExtra = '', isViewportUnits = {}, isXML = {}, isXSLT // dom.xslt.enabled let isBB = false, isBBESR = false, isMB = false, isTB = false, isFPPFallback = false // helps track FPP health, block BB giving passes to FPP protections // region let languagesSupported = {}, localesSupported = {}, isLanguageSmart = false, isLanguagesNav = [], // lowercase sorted to compare to systemLanguages isLocaleValid, isLocaleValue, isLocaleAlt, // allow variants in checks e.g. en-CA checks en-US values isTimeZoneValid, isTimeZoneValue, // intl.locale oIntlLocale = {}, oIntlLocaleKeys = {}, oIntlLocalePerf = {}, // intl.date oIntlDate = {}, oIntlDateKeys = {}, // test dates oIntlDates = {} // other let aDomRect = [true, true, true, true], isDomRect = 0, // default non-gecko oDomRect = {}, isDecimal = false, isPerf = false, isPointerRawUpdate = 'undefined' // overlay metrics let overlayScope = 'document', overlayFP = '_list', overlayHealth = '_summary', overlaySection = '', overlayName = '', overlayCharLen, // length per monospace character overlayInfo = '', metricsData, metricsTitle, metricsPrefix = '' // runtypes let gt0, gt1, gLoad = true, gRun = true, gClick = true, gFS = false, // don't run FS measurements if already tiggered gClear = true, // clear console of xml and BB's prototype/proxy errors isAllowNonGecko = true, // allow some other engines isAllowNonGeckoMin = true, // enforce min requirements on those other engines isAllowNonGeckoUndefined = true, // allow undefined engines isBlock = true, isFontSizesMore = false, // when true: force 3-pass and group/order by name then generic-font-family isFontSizesPrevious = false, isSmart = false, isSmartDataMode = false, // when in data-only mode we still want to run proxy/prototype lies isSmartAllowed = false, // data-only mode - do not give off false health signals if not maintained isStop = false const isBlockMin = 128, isSmartMin = 140 /** DEV **/ // simulate errors let runSG = false, // break globals runST = false, // throw type runSI = false, // throw invalid runSE = false, // generic if not thrown runTE = false, // cause timeout runSF = false, // font enumeration tests runSL = false // lies ================================================ FILE: js/iframes.js ================================================ 'use strict'; const getDynamicIframeWindow = ({ context, source ='', test ='', contentWindow = false, nestIframeInContainerDiv = false, violateSOP = true, // SameOriginPolicy display = false }) => new Promise(resolve => { try { if (runSE) {foo++} const elementName = nestIframeInContainerDiv ? 'div' : 'iframe' const length = context.length const element = document.createElement(elementName) document.body.appendChild(element) if (!display) { element.setAttribute('style', 'display:none') } if (nestIframeInContainerDiv) { const attributes = ` ${source ? `src=${source}` : ''} ${violateSOP ? '' : `sandbox="allow-same-origin"`} ` element.innerHTML = `<iframe ${attributes}></iframe>` } else if (!violateSOP) { element.setAttribute('sandbox', 'allow-same-origin') if (source) { element.setAttribute('src', source) } } else if (source) { element.setAttribute('src', source) } const iframeWindow = contentWindow ? element.contentWindow : context[length] if ('agent' == test) { let newNav = iframeWindow.navigator let data = {} function exit(value) { try {document.body.removeChild(element)} catch(e) {} data['useragentdata'] = value return resolve({'data': data, 'hash': mini(data)}) } // useragent let list = ['appCodeName','appName','appVersion','buildID','oscpu', 'platform','product','productSub','userAgent','vendor','vendorSub'] let tmpData = {}, r list.forEach(function(p) { try { r = newNav[p] let typeCheck = typeFn(r, true), expectedType = 'string' if (!isGecko) { // type check will throw an error for a string "undefined" if ('buildID' == p || 'oscpu' == p) {expectedType = 'undefined'} } if (expectedType !== typeCheck) {throw zErr} if ('' == r) {r = 'empty string'} } catch(e) { r = e } tmpData[p] = r+'' }) data['useragent'] = {'hash': mini(tmpData), 'metrics': tmpData} // useragentdata try { let k = newNav.userAgentData let typeCheck = typeFn(k, true) if ('undefined' == typeCheck) { exit(typeCheck) } else { if ('object' !== typeCheck) {throw zErr} if ('[object NavigatorUAData]' !== k+'') {throw zErr} navigator.userAgentData.getHighEntropyValues([ 'architecture','bitness','brands','formFactors','fullVersionList','mobile', 'model','platform','platformVersion','uaFullVersion','wow64' ]).then(res => { exit({'hash': mini(res), 'metrics': res}) }).catch(function(err){ exit(zErr) }) } } catch(e) { exit(zErr) } } else { try {document.body.removeChild(element)} catch(e) {} return resolve('not supported') } } catch(e) { return resolve(e+'') } }) function get_agent_iframes(log = false) { if (gRun && sectionIgnore.includes('agent')) {return} // runs post FP let t0 = nowFn() let aNames = ['content_docroot', 'content_with_url', 'window_docroot', 'window_with_url', 'iframe_access', 'nested', 'window_access'] let METRIC = 'agent' // get data Promise.all([ getDynamicIframeWindow({context: window, contentWindow: true, violateSOP: false, test: METRIC}), // docroot contentWindow getDynamicIframeWindow({context: window, contentWindow: true, source: '?', violateSOP: false, test: METRIC}), // with URL contentWindow getDynamicIframeWindow({context: window, violateSOP: false, test: METRIC}), // docroot getDynamicIframeWindow({context: window, source: '?', violateSOP: false, test: METRIC}), // with URL getDynamicIframeWindow({context: frames, test: METRIC}), // iframe access getDynamicIframeWindow({context: window, nestIframeInContainerDiv: true, test: METRIC}), // nested getDynamicIframeWindow({context: window, test: METRIC}), // window access ]).then(function(results){ const ctrlHash = mini(sDetail.document.agent_reported) /* test some errors results[0] = 'i am not groot' results[2] = 'i am groot' //*/ /* test some different hashes results[4].data.appCodeName = 'Godzilla' let tmpHash = mini(results[4].data) results[4].hash = tmpHash results[6].data.appName = 'Navigator' tmpHash = mini(results[6].data) results[6].hash = tmpHash //*/ let oData = {}, oDisplay = {}, oHashes = {}, countErrors = 0 for (let i=0; i < results.length; i++) { let item = results[i] let name = 'agent_'+ aNames[i] if ('string' == typeof item) { dom[name].innerHTML = item countErrors++ } else { if (oHashes[item.hash] == undefined) { oHashes[item.hash] = {'group': [name], 'data': item.data} } else { oHashes[item.hash]['group'].push(name) } } } let summary ='', btn ='', errString ='' if (countErrors > 0 && countErrors < results.length) { errString += " <span class='s2'>["+ countErrors +' error'+ (countErrors > 1 ? 's' : '') +']</span>'+ sc } if (countErrors == results.length) { // all errors summary = zErr } else if (Object.keys(oHashes).length == 1) { // single hash let singleHash, notation = match_green for (const k of Object.keys(oHashes)) { singleHash = k if (k !== ctrlHash) { notation = match_red btn = addButton(2, 'agent_iframe', 'details', 'btnc', zIFRAME) addDetail('agent_iframe', oHashes[k].data, zIFRAME) } let items = oHashes[k].group items.forEach(function(item) {oDisplay[item] = k}) } summary = singleHash + btn + notation } else { // multiple hashes for (const k of Object.keys(oHashes)) { let items = oHashes[k].group for (let i=0; i < items.length; i++) { // reset btn = '' let name = items[i] // 1st of each non-match: add details if (i == 0 && k !== ctrlHash) { btn = addButton(2, name, 'details', 'btnc', zIFRAME) addDetail(name, oHashes[k].data, zIFRAME) } oDisplay[name] = k + btn } summary = 'mixed'+ match_red } } for (const k of Object.keys(oDisplay)) {dom[k].innerHTML = oDisplay[k]} dom.uaIframes.innerHTML = summary + errString if (log) {log_perf(SECTNF, 'agent iframes', t0)} }) } countJS('iframes') ================================================ FILE: js/misc.js ================================================ 'use strict'; /* TIMING */ function check_timing(type) { let aAllow = ['currenttime', 'date', 'mark', 'now', 'timestamp'] if (!aAllow.includes(type)) {return true} let setTiming = new Set(), value, result = true let aIgnore = [0, -0, -1, -16, -17] let max = isPerf ? 10 : 500 for (let i=1; i < max ; i++) { try { if ('now' == type) {value = performance.now() - performance.now() } else if ('timestamp' == type) { value = new Event('').timeStamp - new Event('').timeStamp } else if ('date' == type) { value = (new Date())[Symbol.toPrimitive]('number') - (new Date())[Symbol.toPrimitive]('number') } else if ('mark' == type) { value = performance.mark('a').startTime - performance.mark('a').startTime } else if ('currenttime' == type) { value = (gTimeline.currentTime) - (gTimeline.currentTime) } value = Math.trunc(value) // we're subtracting the second measurement from the first so any value !== 0/-0 would be negative if (!aIgnore.includes(value)) {result = false} setTiming.add(value) } catch { // we would have already captured errors return true } } // note: sometimes mark + timestamp "noise" mode are not caught // ToDo: if !isPerf and return is true, perhaps we can try something else eg with a known delay if (false == result) {console.log(type, max, setTiming)} return result } function get_timing_mark() { try { let entries = performance.getEntriesByName("a","mark") if (undefined === entries) { throw zD } else { let tmpSet = new Set() entries.forEach(function(obj){ let value = obj.startTime if (undefined !== value) { let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} tmpSet.add(value) } }) let data = Array.from(tmpSet) data = data.sort(function (a,b) { return a-b}) gData.timing.mark = data } } catch(e) { gData.timing.mark = e+'' } } function get_timing_navigation() { // dom.enable_performance_navigation_timing try { let entries = performance.getEntries().find(({entryType})=>entryType==='navigation') if (undefined === entries) { throw zD } else { let aList = ['connectEnd','connectStart','domComplete','domContentLoadedEventEnd','domContentLoadedEventStart', 'domInteractive','domainLookupEnd','domainLookupStart','duration','loadEventEnd','loadEventStart', 'requestStart','responseEnd','responseStart','secureConnectionStart','startTime','unloadEventEnd', 'unloadEventStart','workerStart'] let tmpSet = new Set() aList.forEach(function(k){ let value = entries[k] if (undefined !== value) { let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} tmpSet.add(value) } }) let data = Array.from(tmpSet) data = data.sort(function (a,b) { return a-b}) gData.timing.navigation = data } } catch(e) { gData.timing.navigation = e+'' } } function get_timing_performance() { // dom.enable_performance try { let entries = performance.timing if (0 === entries.loadEventEnd && 0 == entries.navigationStart) { throw zD } else { let aList = ['connectStart','domComplete','domContentLoadedEventEnd','domContentLoadedEventStart', 'domInteractive','domLoading','domainLookupEnd','domainLookupStart','fetchStart','loadEventEnd', 'loadEventStart','navigationStart','redirectEnd','redirectStart','requestStart','responseEnd', 'responseStart','secureConnectionStart','unloadEventEnd','unloadEventStart'] let tmpSet = new Set() aList.forEach(function(k){ let value = performance.timing[k] if (undefined !== value && 0 !== value) { let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} tmpSet.add(value) } }) let data = Array.from(tmpSet) data = data.sort(function (a,b) { return a-b}) gData.timing.performance = data } } catch(e) { gData.timing.performance = e+'' } } function get_timing_resource() { // dom.enable_resource_timing try { let entries = performance.getEntriesByType('resource') if (0 === entries.length) { if (isFile) {throw zSKIP} else {throw zD} } else { let aList = ['duration','fetchStart','requestStart','responseEnd','responseStart','secureConnectionStart','startTime'] let tmpSet = new Set() entries.forEach(function(obj){ aList.forEach(function(item){ let value = obj[item] if (undefined !== value) { let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} // fix to 3 decimal places: otherwise e.g. // 33.33399999999999, 33.333999999999996 has a diff of 7.105427357601002e-15 // and diff (calc1) becomes 7.1 instead of basically 0 // don't allow huge drift if (value < 200000) { value = value.toFixed(3) * 1 tmpSet.add(value) } } }) }) let data = Array.from(tmpSet) data = data.sort(function (a,b) { return a-b}) gData.timing.resource = data } } catch(e) { gData.timing.resource = e+'' } } function get_timing(METRIC) { let aLoop = ['contexttime','performancetime'] let strZero = 'not enough data' if ('timing_precision' == METRIC) { aLoop = gTiming // check isPerf again if (isPerf) {get_isPerf()} // get a last value for each to ensure a max diff addTimings() // poulate some data get_timing_mark() get_timing_navigation() get_timing_performance() get_timing_resource() /* testing gData.timing.date = [1723240561321] gData.timing.exslt = ['2024-08-09T20:23:10.000','2024-08-09T20:23:11.000'] gData.timing.currenttime = [83.34, 116.72, 150, 233.4] // 60FPS but no 3 decimal places gData.timing.currenttime = [966.686] // only a single RFP entry gData.timing.currenttime = [962.486] // only a single non-RFP entry //ToDo: cleanup rogue is10's and is100's // looking at these they aren't actually 100's but also 50's: but I use divided by 2 gData.timing.currenttime = [86.4, 136.44, 236.48] // real world examples that causes a 100ms entry gData.timing.currenttime = [75.12, 125.16, 225.24] // another one gData.timing.currenttime = [71.12, 121.14, 171.16] // another one gData.timing.exslt = ["2025-07-14T03:53:46.047","2025-07-14T03:53:46.058"] //*/ // isDecimal isDecimal = false if (isPerf) { try { for (let i=0; i < gData.timing.now.length; i++) { if (!Number.isInteger(gData.timing.now[i])) {isDecimal = true; break} } } catch(e) {console.log(e+'')} } } let oGood = { 'date': [0, 1, 16, 17, 33, 34], 'instant': [0, 1, 16, 17, 33, 34], 'resource': [0, 1, 2, 3, 16, 17, 18, 19, 33, 34, 35, 36], // resource can have large drift, use integers: e.g. wait ages then rerun 'performance': [0, 1, 16, 17, 33, 34], 'exslt': [0], // 1912129: exslt diffs must be 1000, and all end in .000 'other': [ // tested 20/10mn timestamps over ~12s/6s = ~750/370 unique times // the longer since the first time, the more decimal points drift // so 0's become 0.1's then 0.2's etc: 6s seems to limit drift to 1 decimal point 0, 0.1, 0.2, 0.3, 0.4, 16.6, 16.7, 16.8, 16.9, 17, 33.3, 33.4, 33.5, 33.6, 33.7, ], 'ten': [0, 10, 20, 30, 40], } let aNotInteger = ['mark','now','timestamp'] let calc1 = new RegExp('^-?\\d+(?:\.\\d{0,' + (1 || -1) + '})?') let str, data, notation, oData = {}, countFail = 0, countErr = 0 sDetail[isScope][METRIC +'_data'] = {} let isDateNoise = false aLoop.forEach(function(k){ let aGood = oGood[k] if (undefined == aGood) {aGood = oGood.other} // don't add to health, we do that with the parent metric str ='', notation = silent_red try { let aTimes = gData.timing[k] if ('string' == typeof aTimes) {throw aTimes} aTimes = dedupeArray(aTimes) if (aTimes.length) {sDetail[isScope][METRIC +'_data'][k] = {'data': aTimes}} // type check let start = aTimes[0] let expected = ('exslt' == k || 'instant' == k) ? 'string' : 'number' let typeCheck = typeFn(start) if (expected !== typeCheck) {throw zErrType + typeCheck} // check noise let isNoise = 'exslt' == k ? false : !check_timing(k) let isMatch = true if (aNotInteger.includes(k) && Number.isInteger(start)) {isMatch = false} if ('date' == k) { isDateNoise = isNoise } else if ('exslt' == k) { isNoise = isDateNoise if ('.000' !== start.slice(-4)) {isMatch = false} // we use epoch time so each entry is always moving forward in time | remove leading 0 in ms start = start.slice(0,20) + start.slice(-2)+ '0' start = (new Date(start))[Symbol.toPrimitive]('number') } else if ('instant' == k) { start = start.slice(0,-1) // remove trailing Z start = (new Date(start))[Symbol.toPrimitive]('number') } else if ('resource' == k) { start = Math.floor(start) } let startIncremental = start // get diffs let isZero = false, is10 = true, is100 = true if ('exslt' == k) {is100 = false} // exslt can only be 10 or RFP let setDiffs = new Set(), setIncremental = new Set(), aTotal = [] if (1 == aTimes.length) { aTotal.push(0) // make sure we display something // all non-exslt we expect multiple values if ('exslt' !== k) {isMatch = false} isZero = true } for (let i=1; i < aTimes.length ; i++) { let end = aTimes[i] // type check typeCheck = typeFn(end) if (expected !== typeCheck) {throw zErrType + typeCheck} // clean up if ('exslt' == k) { if ('.000' !== end.slice(-4)) {isMatch = false} end = end.slice(0,20) + end.slice(-2)+ '0' end = (new Date(end))[Symbol.toPrimitive]('number') } else if ('instant' == k) { end = end.slice(0,-1) // remove trailing Z end = (new Date(end))[Symbol.toPrimitive]('number') } else if ('resource' == k) { end = Math.floor(end) } // truncate to 1 decimal place // diffs since start let totaldiff = ((end - start).toString().match(calc1)[0]) * 1 aTotal.push(totaldiff) let diff = (totaldiff % 50).toFixed(2) * 1 // drop 50s setDiffs.add(diff) // incremental diffs: helps reduce drift totaldiff = ((end - startIncremental).toString().match(calc1)[0]) * 1 diff = (totaldiff % 50).toFixed(2) * 1 // drop 50s setIncremental.add(diff) startIncremental = end // set this end value for the next incremental start value } // diff arrays let aDiffs = Array.from(setDiffs) let aIncremental = Array.from(setIncremental) sDetail[isScope][METRIC +'_data'][k]['diffs'] = aDiffs sDetail[isScope][METRIC +'_data'][k]['incremental'] = aIncremental // using incremental: test intervals if (aIncremental.length) { for (let i=0; i < aIncremental.length; i++) { if (isMatch && !aGood.includes(aIncremental[i])) {isMatch = false} if (is10 && !oGood.ten.includes(aIncremental[i])) {is10 = false} if (is100 && 0 !== aIncremental[i]) {is100 = false} } } else { // a single data entry means zero diffs/incremental == so we can never get isMatch // and is10/is100 can be false positives (we can ignore - seems rare) // don't assume 10 or 100 if only 1 sample size // ToDo: I do not like this: rare? and a false positive is ok when you look at the rest of the data // we could analayse and cleanup data afterwards } // some tests we can rely on non-integer // but others we measure enough to not all land on 0's (or 50's and 100s) let a100 = ['date','performance','contexttime','performancetime'] if (a100.includes(k)) {if (is100 || is10) {isMatch = false}} // clean up exslt if ('exslt' == k) { if (isNoise) { is100 = false if ('10ms' !== oData['date']) {is10 = false} } } // currenttime: 60FPS false positives: check for 3 decimal places if (isMatch && 'currenttime' == k) { isMatch = false for (let i=0; i < aTimes.length; i++) { let check = Math.floor(aTimes[i]) === aTimes[i] ? 0 : (aTimes[i]).toString().split(".")[1].length if (3 === check) {isMatch = true; break} } } //console.log(k, isNoise) let value ='' if (isMatch && !isNoise && isGecko) { notation = silent_green value = 'RFP' } else { // add entropy e.g. jShelter 10ms or 100ms or noise // order is 100+, 100, 10, noise, nothing // isZero could be is100: sometimes we just don't get enough data // so it can be a little unstable with e.g. extension fuckery - that's OK if (isZero) {value = strZero } else if (is100) {value = '100ms' } else if (is10) {value = '10ms' } else if (isNoise) {value = 'noise' } countFail++ } oData[k] = value // display str = aTotal.join(', ') let strLen = str.length if (strLen > 60) { let len = aTotal.length, unitLen = strLen/(aTotal.length) let reduce = Math.floor((str.length - 60)/unitLen) let lasttwo = ' &#x2026 '+ aTotal[len-2] +', '+aTotal[len-1] let newTotal = aTotal.slice(0, len - (reduce + 2)) if ((newTotal.join(', ') + lasttwo).length > 60) {newTotal = newTotal.slice(0, len - (reduce + 3))} str = newTotal.join(', ') + lasttwo } //console.log(k, is10, is100, data, aTotal) //console.log(k, '\naDiffs', aDiffs, '\naIncremental', aIncremental) } catch(e) { oData[k] = '' if ('reducetimer' !== k) { if ('instant' == k) { // gecko 139+ we expect temporal to work if (!isGecko || isGecko && isVer < 139) { if ('ReferenceError: Temporal is not defined' == e || 'ReferenceError: Can\'t find variable: Temporal' == e) {e = zSKIP} } } str = (zD == e || zSKIP == e) ? e : log_error(17, METRIC +'_'+ k, e) oData[k] = (zD == e || zSKIP == e) ? e : zErr if (zSKIP !== e) { countFail++ countErr++ } else { notation = '' } } } //sDetail[isScope][METRIC][k] = data if ('timing_precision' == METRIC) { if ('reducetimer' !== k) { addDisplay(17, METRIC +'_'+ k, str,'', notation) } } else { addDisplay(17, METRIC +'_'+ k, str,'', notation) } }) // display let btn = '' // data if ('timing_precision' == METRIC) { // we didn't countFail reducetimer or skipped Temporal so RFP will have zero fails let rtvalue // reducetimer: privacy.reduceTimerPrecision notation = silent_green let isProtected = (aLoop.length - countFail) == aLoop.length // currenttime: handle a single data entry if (1 == countFail && oData.currenttime == strZero) { rtvalue = zNA // with RFP is always oGood try { let singledata = (gData.timing.currenttime[0].toString().match(calc1)[0]) * 1 let singletest = (singledata % 50).toFixed(2) * 1 if (oGood.other.includes(singletest)) { oData.currenttime = 'RFP' addDisplay(17, METRIC +'_currenttime', singledata,'', notation) countFail = countFail - 1 isProtected = (aLoop.length - countFail) == aLoop.length // recalc } } catch(e) {} } // if _some_ RFP timing fails (i.e isProtected !== aLoop.length), then rtvalue // can end up false when it isn't really - because we have decimals but cannot // know for sure it's RFP (not worth checking everything non-error for 60FPS) // but what we can do is exclude all errors isProtected = (aLoop.length - countFail) + countErr == aLoop.length //console.log(aLoop.length - countFail, countErr, aLoop.length, isProtected) if (isProtected && isDecimal || !isGecko) { // non-Gecko || if RFP which is also isDecimal, then we can't tell rtvalue = zNA // RFP with decimals looks silly if (isGecko) {isDecimal = false} } else { rtvalue = !isDecimal if (isDecimal) {countFail++; notation = silent_red} } // reducetimer data/display addDisplay(17, METRIC +'_reducetimer', rtvalue,'', notation) oData['reducetimer'] = rtvalue // recalc isProtected = (aLoop.length - countFail) == aLoop.length notation = isProtected ? rfp_green : rfp_red str = (aLoop.length - countFail) +'/' + aLoop.length // add btn = addButton(17, METRIC, str) + addButton(17, METRIC +'_data', 'data') addBoth(17, METRIC, mini(oData), btn, notation, oData) // cleanup //performance.clearMeasures() } else { notation = (aLoop.length - countFail) == aLoop.length ? rfp_green : rfp_red str = (aLoop.length - countFail) +'/' + aLoop.length btn = addButton(17, METRIC, str) + addButton(17, METRIC +'_data', 'data') sDetail[isScope][METRIC] = oData addDisplay(17, METRIC, mini(oData), btn, notation) } return } /* MISC */ function check_mathLies(type) { let mathList = ['Math.cos','Math.sin','Math.tan'] // trig if ('other' == type) { mathList = ['Math.cosh','Math.exp','Math.log','Math.pow'] } /* // not used 'Math.acos','Math.acosh','Math.asinh','Math.atan','Math.atan2','Math.atanh', 'Math.cbrt','Math.expm1','Math.log10','Math.log1p','Math.sinh','Math.sqrt','Math.tanh' */ return mathList.some(lie => sData[SECT99].indexOf(lie) >= 0) } function get_component_shims(METRIC) { if (!isGecko) {addBoth(18, METRIC, zNA); return} // 960392: dom.use_components_shim let hash, btn ='', data, notation = isBB ? bb_red: '' try { data = Object.keys(Object.getOwnPropertyDescriptors(Components.interfaces)) hash = mini(data); btn = addButton(18, METRIC, data.length) } catch(e) { if (e+'' == 'ReferenceError: Components is not defined') { if (isBB) {notation = bb_green} hash = 'undefined' } else { hash = e; data = zErrLog } } addBoth(18, METRIC, hash, btn, notation, data) return } function get_math(METRIC, isLies) { let hash, btn='', data = {}, notation = 'math_trig' == METRIC ? rfp_red : '' try { if ('math_trig' == METRIC) { const oMath = { cos: [ ['-1',-1],['17*Math.LOG10E',17*Math.LOG10E],['1e12',1e12],['1e130',1e130], ['1e140',1e140],['1e251',1e251],['1e272',1e272],['1e284',1e284],['1e75',1e75], ['21*Math.LN2',21*Math.LN2],['21*Math.LOG2E',21*Math.LOG2E],['25*Math.SQRT2',25*Math.SQRT2], ['50*Math.SQRT1_2',50*Math.SQRT1_2],['51*Math.LN2',51*Math.LN2],['57*Math.E',57*Math.E], ], sin: [ ['35*Math.SQRT1_2',35*Math.SQRT1_2],['7*Math.LOG10E',7*Math.LOG10E], ], tan: [ ['10*Math.LOG10E',10*Math.LOG10E],['10*Math.LOG2E',10*Math.LOG2E], ['17*Math.SQRT2',17*Math.SQRT2],['34*Math.SQRT1_2',34*Math.SQRT1_2], ['6*Math.E',6*Math.E],['6*Math.LN2',6*Math.LN2], ], } const keys = ['cos','sin','tan'] for (let x = 0; x < keys.length; x++){ let k = keys[x] oMath[k].forEach(function(item) {data['Math.'+ k +'('+ item[0] +')'] = Math[k](item[1])}) } } else { data = { '(Math.E - 1 / Math.E) / 2': (Math.E - 1 / Math.E) / 2, // sinh(1) '(Math.exp(1) + Math.exp(-1)) / 2': (Math.exp(1) + Math.exp(-1)) / 2, // cosh(1) '(Math.exp(1) - Math.exp(-1)) / 2': (Math.exp(1) - Math.exp(-1)) / 2, // sinh(1) alt 'Math.E - 1': Math.E - 1, // expm1(1) 'Math.cosh(1)': Math.cosh(1), 'Math.exp(1) - 1': Math.exp(1) - 1, // expm1(1) alt 'Math.log((1.5)/(0.5))/2': Math.log((1.5) / (0.5)) / 2, 'Math.pow(Math.abs(Math.PI), 1 / 3)': Math.pow(Math.abs(Math.PI), 1 / 3), // polyfill cbrt(Math.PI) } } if (runST) {if ('math_trig' == METRIC) {data['Math.cos(-1)'] = NaN} else {data['Math.E - 1'] = Infinity}} for (const k of Object.keys(data)) { let typeCheck = typeFn(data[k]) if ('number' !== typeCheck) {throw zErrType + typeCheck} } hash = mini(data); btn = addButton(18, METRIC) if (METRIC == 'math_trig' && 'd240b02e' == hash) {notation = rfp_green} } catch(e) { hash = e; data = zErrLog } addBoth(18, METRIC, hash, btn, notation, data, isLies) return } function get_math_css(METRIC) { // 1881277: ToDo: expand this // This should just be equivalancy of Math but IDK what libraries CSS uses // and Math has entropy beyond platform architecture // We could also try to replicate // - math_trig: RFP doesn't cover this IIUIC so a leak? // - math_other // both would add more methods and expose lies/leaks let hash, btn='', data = {}, isLies = isDomRect == -1 let aList = ['A'] try { let wrapper = dom.tzpCalc, target = dom.tzpCalcTarget, method aList.forEach(function(k){ target.className = '' // remove any classes target.classList.add('tzpCalc' +k) // add class method = measureFn(wrapper, METRIC) if (k == aList[0]) { // typeCheck first itemn if (undefined !== method.error) {throw method.errorstring} let value = method.width if (runST) {value = undefined} let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} } data[k] = method.width }) hash = mini(data); btn = addButton(18, METRIC) } catch(e) { hash = e; data = zErrLog } addBoth(18, METRIC, hash, btn, '', data, isLies) } function get_navigator_keys(METRIC) { let hash, btn='', aNav = [], notation = isBBESR ? bb_red : '', isLies = false try { if (runST) {foo++} // navigator for (const key in navigator) {aNav.push(key)} let typeCheck = typeFn(aNav) if ('array' !== typeCheck) {throw zErrType + typeCheck} if (isSmart) { // navigator.prototype: should match navigator let aProto = Object.keys(Object.getOwnPropertyDescriptors(Navigator.prototype)) let typeCheck = typeFn(aProto) if ('array' !== typeCheck) {throw zErrType + typeCheck} // ToDo: check/expand these let expected = [ 'appCodeName','appName','appVersion','buildID','oscpu','platform','product','productSub','userAgent','cookieEnabled', 'vendor','vendorSub','hardwareConcurrency','language','languages','mimeTypes','onLine','plugins','webdriver', 'taintEnabled','javaEnabled','doNotTrack','cookieEnabled','pdfViewerEnabled','requestMediaKeySystemAccess', 'locks', // 1851539 'userActivation', // 1791079 // ToDo: FF144+? 1983296 functionality pref deprecated //'globalPrivacyControl', // 1983296 ] // test if (runSL) { expected = ['a','b','javaEnabled'] aNav = ['a','javaEnabled','c','d','f','g'] // pre a, missing b, a+d not-in-proto aProto = ['javaEnabled','b','c','e','constructor','f','g'] // missing a, post f+g, e not in nav } // compare hashes let navhash = mini(aNav.concat('constructor')), protohash = mini(aProto) // do I need this // tampering: don't dedupe, just collect let missing = {}, post = [], pre = [], diffs = {}, oTampered = {} // aProto: post constructor let position = aProto.indexOf('constructor') post = aProto.slice(position +1) // aNav: pre javaEnabled position = aNav.indexOf('javaEnabled') if (position > 0) { pre = aNav.slice(0, position) if (isVer < 129) {pre = pre.filter(x => !['vibrate'].includes(x))} // ignore vibrate in 128 or lower } // missing let missingNav = expected.filter(x => !aNav.includes(x)) let missingProto = expected.filter(x => !aProto.includes(x)) if (missingNav.length) {missing['navigator'] = missingNav.sort()} if (missingProto.length) {missing['prototype'] = missingProto.sort()} // diffs // in prototype but not in nav let notNav = aProto.filter(x => !aNav.includes(x)) notNav = notNav.filter(x => !['constructor'].includes(x)) // ignore constructor if (notNav.length) {diffs['not_in_navigator'] = notNav.sort()} // in nav but not in prototype let notProto = aNav.filter(x => !aProto.includes(x)) if (notProto.length) {diffs['not_in_prototype'] = notProto.sort()} // isLies = (post.length + pre.length + Object.keys(missing).length + Object.keys(diffs).length) > 0 if (isLies) { if (Object.keys(missing).length) {oTampered['missing_expected'] = missing} if (post.length) {oTampered['post_constructor'] = post.sort()} if (pre.length) {oTampered['pre_javaEnabled'] = pre.sort()} if (Object.keys(diffs).length) {oTampered['prototype_vs_navigator'] = diffs} addDetail(METRIC +'_tampered', oTampered) } } // always return aNav hash = mini(aNav); btn = addButton(18, METRIC, aNav.length) // health: BB only if ESR if (isBBESR) { if (isMB) { // MB if ('a389214b' == hash) {notation = bb_green} // MB15 41 } else { // TB if ('8d3dd2a1' == hash) {notation = bb_green} // TB15 } } // if tampered use notation to fail health if (isLies) {notation += addButton('bad', METRIC +'_tampered', "<span class='health'>"+ cross + '</span> tampered')} } catch(e) { hash = e; aNav = zErrLog } addBoth(18, METRIC, hash, btn, notation, aNav, isLies) return } function get_pdf(METRIC) { // FF99+ none/hardcoded: all three are expected nav keys // https://html.spec.whatwg.org/multipage/system-state.html#dom-plugin // the order is alphabetical with 'PDF Viewer' inserted in the 0th popsition let data = {} function get_obj(item) { let res = 'none' try { let obj = navigator[item] if (runST) {obj = []} else if (runSI) {obj = {}} let typeCheck = typeFn(obj, true) if ('object' !== typeCheck) {throw zErrType + typeFn(obj)} let expected = '[object '+ item.charAt(0).toUpperCase() + (item.slice(1)).slice(0, -1) +'Array]' if (expected !== obj+'') {throw zErrInvalid +'expected '+ expected +': got '+ obj+''} let cyclicTest = mini(obj) // TypeError: cyclic object if (obj.length) { res = [] for (let i=0; i < obj.length; i++) { if ('mimeTypes' == item) { res.push( obj[i].type + (obj[i].description == '' ? ': * ' : ': '+ obj[i].type) + (obj[i].suffixes == '' ? ': *' : ': '+ obj[i].suffixes)) } else { res.push(obj[i].name + (obj[i].filename == '' ? ': * ' : ': '+ obj[i].filename) + (obj[i].description == '' ? ': *' : ': '+ obj[i].description)) } } } } catch(e) { log_error(18, METRIC +'_'+ item, e); res = zErr } data[item] = res return } function get_pdfViewer(item) { let res try { res = navigator[item] if (runST) {res = undefined} let typeCheck = typeFn(res) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} } catch(e) { log_error(18, METRIC +'_'+ item, e); res = zErr } data[item] = res return } Promise.all([ get_obj('mimeTypes'), // do in sorted order get_pdfViewer('pdfViewerEnabled'), get_obj('plugins'), ]).then(function() { // FF116 1838415 dropped RFP protection // FF147 1999126 re-added: just notate as RFP regardless of version let notation = rfp_red, isLies = false if (runSL) {data = {'mimeTypes': 'none', 'pdfViewerEnabled': true, 'plugins': 'none'}} let hash = mini(data) if (!['91073152','beccb452'].includes(hash) || isProxyLie('Navigator.pdfViewerEnabled')) { isLies = true } else { try { let keys = Object.keys(Object.getOwnPropertyDescriptors(Navigator.prototype)) if (keys.indexOf('pdfViewerEnabled') > keys.indexOf('constructor')) {isLies = true} } catch {} } if ('91073152' == hash) {notation = rfp_green} addBoth(18, METRIC, hash, addButton(18, METRIC), notation, data, isLies) return }) } const get_speech_engines = (METRIC) => new Promise(resolve => { // media.webspeech.synth.enabled let t0 = nowFn(), notation = rfp_red, isLies = false function exit(display, value) { addBoth(18, METRIC, display,'', notation, value, isLies) return resolve() } function populateVoiceList() { let res = [], ignoreLen, ignoreStr /* examples moz-tts:android:hr_HR urn:moz-tts:sapi:Microsoft David - English (United States)?en-US urn:moz-tts:osx:com.apple.eloquence.en-US.Eddy */ let aStrip = [ 'moz-tts:android:', // android 'urn:moz-tts:osx:com.apple.eloquence.', // mac 'urn:moz-tts:sapi:', // windows ] try { let v = speechSynthesis.getVoices() if (runST) {v = null} else if (runSI) {v = [{}]} else if (runSL) {addProxyLie('speechSynthesis.getVoices')} let typeCheck = typeFn(v, true) if ('array' !== typeCheck) {throw zErrType + typeFn(v)} if (v.length) { let expected = '[object SpeechSynthesisVoice]' if ((v +'').slice(0,29) !== '[object SpeechSynthesisVoice]') {throw zErrInvalid +'expected '+ expected} } if (v.length == 0) { notation = rfp_green exit('none','none') } else { // enumerate: reduce redundancy/noise // only record default if true and localService if false | ignore voiceURI if it matches expected v.forEach(function(i) { let uriStr = i.voiceURI, skipURI = false // replace useless strings aStrip.forEach(function(str){uriStr = uriStr.replace(str, '')}) uriStr.trim() if (isGecko) { skipURI = (uriStr = i.name +'?'+ i.lang) // windows } else { // blink skipURI = (uriStr == i.name) // dedupe repetitive crap } res.push( i.name +' | '+ i.lang + (i['default'] ? ' | default' : '') + (i.localService ? '' : ' | false') + (skipURI ? '' : ' | '+ uriStr) ) }) let hash = mini(res) addBoth(18, METRIC, hash, addButton(18, METRIC, res.length), notation, res, isProxyLie('speechSynthesis.getVoices')) log_perf(18, METRIC, t0) return resolve() } } catch(e) { exit(e, zErrLog) } } try { populateVoiceList() if (speechSynthesis.onvoiceschanged !== undefined) { speechSynthesis.onvoiceschanged = populateVoiceList; } } catch(e) { exit(e, zErrLog) } }) function get_svg(METRIC) { let hash, data ='', target = dom.tzpSVG try { if (runSE) {foo++} target.innerHTML ='' const svgns = 'http://www.w3.org/2000/svg' let shape = document.createElementNS(svgns,'svg') let rect = document.createElementNS(svgns,'rect') rect.setAttribute('width',20) rect.setAttribute('height',20) shape.appendChild(rect) target.appendChild(shape) hash = target.offsetHeight > 0 ? zE : zD } catch(e) { hash = e; data = zErrLog } addBoth(18, METRIC, hash,'','', data) return } function get_webdriver(METRIC) { // expected FF60+ let value, data ='' try { value = navigator[METRIC] if (runST) {value = null} let typeCheck = typeFn(value) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} } catch(e) { value = e; data = zErrLog } addBoth(18, METRIC, value,'','', data, isProxyLie('Navigator.'+ METRIC)) return } function get_window_functions(METRIC) { // isProps was generated n+ sorted in get_window_props let t0 = nowFn() let hash, btn='', tamperBtn ='', data = {}, oPost = (isGecko ? {} : ''), notation = isBBESR ? bb_red : '' let typeCheck = typeFn(isProps) // propagate errors if ('string' == typeCheck) { addBoth(18, METRIC, isProps, '', notation, zErrLog) return } // isProps _should_ be an error string of a populated array, but let's cover it if ('array' !== typeCheck) { addBoth(18, METRIC, zErrType + typeCheck, '', notation, zErrLog) return } // remove Navigator, CSSStyleProperties (+ CSS2Properties Gecko 143 or lower) isProps = isProps.filter(x => !['CSS2Properties','CSSStyleProperties','Navigator'].includes(x)) // reduce to functions only try { let intPost = 0 isProps.forEach(function(f){ if (window[f] !== undefined) { try { let array = Object.getOwnPropertyNames(window[f].prototype) // ignore constructor only if (1 == array.length && 'constructor' == array[0]) { } else { data[f] = array if (isGecko && 'constructor' !== array[array.length -1]) { oPost[f] = array.slice(array.indexOf('constructor')+1) intPost += oPost[f].length } } } catch(e) {} } }) hash = mini(data); btn = addButton(18, METRIC, Object.keys(data).length) // tampered (or rather post constructor items) let intKeys = Object.keys(oPost).length if (intKeys > 0) { let postName = '_post_constructor' sDetail[isScope][METRIC + postName] = oPost tamperBtn = addButton(18, METRIC + postName, intKeys +'/'+ intPost) } // notation: safer vs standard doesn't seem to affect this // but we need to check webgl click to play if (isBBESR) { // hashes must be calculated on HTTPS not file schema let oHashes = { // key: [standard, safer] MB : { 'linux': ['c99980b4','12add862'], 'mac': ['f862f015','df5f9ac3'], 'windows': ['c99980b4','12add862'] }, TB : { 'linux': ['0c9aaf28','cdde2f4c'], 'mac': ['efef2c31','d2e0c655'], 'windows': ['0c9aaf28', 'cdde2f4c'] }, } let key = isTB ? 'TB' : 'MB' if (undefined !== oHashes[key][isOS]) { if (oHashes[key][isOS].includes(hash)) {notation = bb_green} } } } catch(e) { hash = e; data = zErrLog } addBoth(18, METRIC, hash, btn + tamperBtn, notation, data) log_perf(18, METRIC, t0) return } function get_window_prop(METRIC) { // BB: display only: wasm let str, notation = isBB ? bb_slider_red : '' try { str = window.WebAssembly if (runST) {str = null} let typeCheck = typeFn(str) if ('undefined' !== typeCheck && 'object' !== typeCheck) {throw zErrType + typeCheck} str = ('object' === typeCheck) ? zE : zD if (isBB) {notation = str == zE ? bb_standard : bb_safer} } catch(e) { str = log_error(18, METRIC, e) } addDisplay(18, METRIC, str,'', notation) return } function get_window_props(METRIC) { /* https://github.com/abrahamjuliot/creepjs */ let t0 = nowFn(), iframe let hash, btn='', data, dataLen, dataSorted, dataOriginal, notation = isBBESR ? bb_red : '' let tamperHash = zNA, tamperBtn ='', aTampered ='' let oIndex = {}, isConsoleOpen = false, allowConsole = false, isAlert = false, skipAlert = false isProps = [] // reset: used to build function proprties let id = 'iframe-window-version' try { // create & append let el = document.createElement('iframe') el.setAttribute('id', id) el.setAttribute('style', 'display: none') /* ToDo: investigate if ('blink' == isEngine) { // content scripts never run in frames like this // javascript:undefined, javascript:null as long as it's not a string el.setAttribute('src', 'javascript:false') } //*/ if (!runSE) {document.body.appendChild(el)} // get props iframe = dom[id] let contentWindow = iframe.contentWindow // data to manipulate data = Object.getOwnPropertyNames(contentWindow) dataLen = data.length // original: useful for analysis dataOriginal = Object.getOwnPropertyNames(contentWindow) sDetail[isScope][METRIC +'_original'] = dataOriginal // sorted: we use this in function_props isProps = Object.getOwnPropertyNames(contentWindow) isProps.sort() if (isGecko) { // get index positions let aIndex = ['Event','PageTransitionEvent','Performance','PerformanceTiming','console'] aIndex.forEach(function(item){let x = data.indexOf(item); oIndex[item] = [x, dataLen - x]}) oIndex['total_count'] = dataLen // all the properties that can be tampered with by NS/uBO // ultimately we move those present (sorted) to the end of the data so we can get a stable hash across security levels in Base Barowser // FF148+ 543435 changed things up let isExpanded = isVer > 147 let aExpanded = ['PerformanceTiming','console','Promise','PageTransitionEvent','NodeList'] // nodelist from android let aPossible = [ 'Audio','Blob','Crypto','CustomEvent','Element','Error','HTMLAudioElement','HTMLCanvasElement', 'HTMLElement','HTMLFrameElement','HTMLHtmlElement','HTMLIFrameElement','HTMLImageElement', 'HTMLMediaElement','HTMLObjectElement','HTMLVideoElement','Image','MediaSource','NodeList', 'OffscreenCanvas','Promise','Proxy','SecurityPolicyViolationEvent','SharedWorker','String','URL', 'WebAssembly','Worker','XMLHttpRequest','XMLHttpRequestEventTarget','decodeURI','decodeURIComponent', 'encodeURI','encodeURIComponent','escape','unescape','webkitURL', ] if (isExpanded) { aPossible.push( 'JSON','MutationObserver','WebSocket','XMLHttpRequest','XMLHttpRequestEventTarget', // 543435 uBO exposes these 5 'Navigator', // NoScript aded this around 148alpha ) } if (isSmart) { /* determine console state before we start messing around with the array FF147 and lower 'Event' and FF148+ 'console' is low=closed high=open - e.g. FF141 event moves from ~175 to ~935, FF145 from ~417 to ~944 - e.g. FF148 console moves from ~241 to ~962 So checking if it is within 150 or so from the end is a loose ranged check that allows for loads of tampered items. Unfortunately NS + webgl-click-to-play (i.e no exception) and probably other extensions break this rule, so we have to stick with the more rigid position is off-by-one diff checks, and use isAlert to add a 'likely' qualifier */ let threshold = dataLen - 150 if (!isExpanded) { if (oIndex.Event[0] !== -1 && oIndex.Performance[0] !== -1) { // if event hasn't moved (no tampering) then you are definitely closed if (oIndex.Event[0] < threshold) { isConsoleOpen = false; skipAlert = true } else { isConsoleOpen = oIndex.Event[0] == oIndex.Performance[0] + 1; allowConsole = true // old method } } } else { if (oIndex.console[0] !== -1 && oIndex.PerformanceTiming[0] !== -1) { allowConsole = true // if console or PT haven't moved (no tampering) then you are definitely closed if (oIndex.PerformanceTiming[0] < threshold || oIndex.console[0] < threshold) { isConsoleOpen = false; skipAlert = true } else { isConsoleOpen = oIndex.console[0] == oIndex.PerformanceTiming[0] + 1 // new method } } } // tampered: filter items for console open etc // safer closed: Performance ... more items then Event // standard closed: Performance + no Event... // BB/FF/ALL open: Performance then Event... if (runSL) {data.push('fake')} aTampered = data.slice(oIndex.Performance[0] +1) let aIgnore = ['Event','Location'] if (isExpanded) {aIgnore = aIgnore.concat(aExpanded)} aTampered = aTampered.filter(x => !aIgnore.includes(x)) if (aTampered.length) { addDetail(METRIC +'_tampered', aTampered.sort()) tamperBtn = addButton(18, METRIC +'_tampered', aTampered.length + ' tampered') tamperHash = mini(aTampered) } else { aTampered = '' tamperHash = 'none' } } let aHas = aPossible.filter(x => data.includes(x)) aHas = dedupeArray(aHas) aHas.sort() // always move all these to the end // this gives us a much more stable hash with and without NS tampering, but also allows false positives data = data.filter(x => !aHas.includes(x)) data = data.concat(aHas) if (isSmart) { // now we check tampered items not in possible if (aTampered.length) { let aTamperedNotInPossible = aTampered.filter(x => !aPossible.includes(x)) if (aTamperedNotInPossible.length) { isAlert = true // don't record untrustworthy/lies - just collect tampered items // maintaining is too burdensome: also many extensions can add extra tampered items // e.g. AutocopySelection2Clipboard can trigger 'HTMLBodyElement','HTMLHeadElement','Selection' console.log(mini(aTamperedNotInPossible), aTamperedNotInPossible) } } // notate console: mark as likely if additional tampering if (allowConsole && isDesktop) { let strConsole = ' [devtools ' + (isAlert && !skipAlert ? ' likely ': '') + (isConsoleOpen ? 'open' : 'closed') +']' addDisplay(18, 'consolestatus', strConsole) } } // move expected Performance, Event, Location to the end // these affect the order if console open and various tabs selected let aCheck = ['Location','Performance','Event'] if (isExpanded) {aCheck = aCheck.concat(aExpanded)} let aItems = data.filter(x => aCheck.includes(x)) aItems.sort() // because an open console can change the order data = data.filter(x => !aItems.includes(x)) data = data.concat(aItems) } else { data.sort() } hash = mini(data); btn = addButton(18, METRIC, data.length) if (isGecko) { btn += addButton(18, METRIC +'_sorted', 'sorted') + (isDesktop ? addButton(18, METRIC +'_original', 'original') : '') + addButton(18, METRIC +'_index', 'index') sDetail[isScope][METRIC +'_sorted'] = isProps sDetail[isScope][METRIC +'_index'] = oIndex /* recored original order for analysis btn += //*/ } else { btn += addButton(18, METRIC +'_original', 'original') } // health: BB only if ESR if (isBBESR) { // hashes are: standard (has WebAssembly) | safer (should be identical w/ and w/o webgl clicktoplay) // funfact: 1419501 (backported from FF144) radically altered the order of items in the unordered list // NOTE: hashes can not be computed via file schema as NS does weird shit now let oHashes = { MB : { 'linux': ['0a2537c8','3c3cf46a'], // 860, 859 'mac': ['cb4fa24b','8be8b02d'], // 857, 856 'windows': ['0a2537c8','3c3cf46a'] // 860, 859 }, TB : { 'linux': ['f3fd6bb5','0698db17'], // 837, 836 'mac': ['bd279b0a','27f5532c'], // 834, 833 'windows': ['f3fd6bb5','0698db17'] // 837, 836 }, } let key = isTB ? 'TB' : 'MB' if (undefined !== oHashes[key][isOS]) { if (oHashes[key][isOS].includes(hash)) {notation = bb_green} } } } catch(e) { hash = e; data = zErrLog if (isProps.length == 0) {isProps = e+''} } removeElementFn(id) addBoth(18, METRIC +'_tampered', tamperHash, tamperBtn, '', aTampered) if (isAlert) {notation += sb +' *'+ sc} addBoth(18, METRIC, hash, btn, notation, data) log_perf(18, METRIC, t0) return } const outputTiming = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('timing')) {return resolve()} if (!gRun) {return resolve()} /* other perf prefs are in window properties dom.enable_performance_observer: PerformanceObserver, PerformanceObserverEntryList dom.enable_performance_navigation_timing: PerformanceNavigationTiming dom.enable_event_timing: EventCounts, PerformanceEventTiming dom.performance.time_to_contentful_paint.enabled: ? dom.enable_performance: ? dom.enable_resource_timing: doesn't remove property PerformanceResourceTiming */ Promise.all([ get_timing('timing_precision'), ]).then(function(){ return resolve() }) }) const outputMisc = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('misc')) {return resolve()} if (runSL) { addProxyLie('Math.sin') addProxyLie('Math.log') } let isMathTrigLies = check_mathLies('trig') let isMathOtherLies = check_mathLies('other') let notation = '', value = zNA if (isGecko) { // 1259822: FF74+ | 1965165: javascript.options.property_error_message_fix FF140+ default enabled try {null.bar} catch(e) { if (isBB) { notation = (e+'' == 'TypeError: can\'t access property "bar" of null' ? bb_green : bb_red) } value = e.message } } addBoth(18, 'error_message_fix', value,'', notation) Promise.all([ get_svg('svg_enabled'), get_math('math_trig', isMathTrigLies), get_math('math_other', isMathOtherLies), get_math_css('math_css'), get_component_shims('component_interfaces'), get_window_prop('wasm'), get_window_props('window_properties'), get_navigator_keys('navigator_keys'), get_webdriver('webdriver'), get_pdf('pdf'), get_speech_engines('speech_engines'), ]).then(function(){ Promise.all([ get_window_functions('window_functions') ]).then(function(){ return resolve() }) }) }) countJS(18) ================================================ FILE: js/prototypeLies.js ================================================ 'use strict'; /* https://github.com/abrahamjuliot/creepjs - https://abrahamjuliot.github.io/creepjs/tests/prototype.html - https://github.com/abrahamjuliot/creepjs/blob/master/docs/tests/prototype.js */ const outputPrototypeLies = (isResize = false) => new Promise(resolve => { if (isResize) {return resolve()} sData[SECT98] = {} sData[SECT99] = [] if (!isProtoProxy) {return resolve()} let t0 = nowFn() const getIframe = () => { try { const numberOfIframes = window.length const frag = new DocumentFragment() const div = document.createElement('div') frag.appendChild(div) const ghost = () => ` height: 100vh; width: 100vw; position: absolute; left:-10000px; visibility: hidden; ` div.innerHTML = `<div style="${ghost()}"><iframe></iframe></div>` document.body.appendChild(frag) const iframeWindow = window[numberOfIframes] return { iframeWindow, div } } catch (error) { return { iframeWindow: window, div: undefined } } } const { iframeWindow, div: iframeContainerDiv } = getIframe() const getPrototypeLies = scope => { // engine const IS_BLINK = 'blink' == isEngine const IS_GECKO = isGecko const IS_WEBKIT = 'webkit' == isEngine const getRandomValues = () => ( String.fromCharCode(Math.random() * 26 + 97) + Math.random().toString(36).slice(-7) ) const randomId = getRandomValues() // Lie Tests // object constructor descriptor should return undefined properties const getUndefinedValueLie = (obj, name) => { const objName = obj.name const objNameUncapitalized = self[objName.charAt(0).toLowerCase() + objName.slice(1)] const hasInvalidValue = !!objNameUncapitalized && ( typeof Object.getOwnPropertyDescriptor(objNameUncapitalized, name) != 'undefined' || typeof Reflect.getOwnPropertyDescriptor(objNameUncapitalized, name) != 'undefined' ) return hasInvalidValue } // accessing the property from the prototype should throw a TypeError const getIllegalTypeErrorLie = (obj, name) => { const proto = obj.prototype try { proto[name] return true } catch (error) { return error.constructor.name != 'TypeError' ? true : false } } // calling the interface prototype on the function should throw a TypeError const getCallInterfaceTypeErrorLie = (apiFunction, proto) => { try { new apiFunction() apiFunction.call(proto) return true } catch (error) { return error.constructor.name != 'TypeError' } } // applying the interface prototype on the function should throw a TypeError const getApplyInterfaceTypeErrorLie = (apiFunction, proto) => { try { new apiFunction() apiFunction.apply(proto) return true } catch (error) { return error.constructor.name != 'TypeError' } } // creating a new instance of the function should throw a TypeError const getNewInstanceTypeErrorLie = apiFunction => { try { new apiFunction() return true } catch (error) { return error.constructor.name != 'TypeError' } } // extending the function on a fake class should throw a TypeError and message "not a constructor" const getClassExtendsTypeErrorLie = apiFunction => { try { const shouldExitInSafari13 = ( /version\/13/i.test((navigator || {}).userAgent) && IS_WEBKIT ) if (shouldExitInSafari13) { return false } // begin tests class Fake extends apiFunction { } return true } catch (error) { // Native has TypeError and 'not a constructor' message in FF & Chrome return ( error.constructor.name != 'TypeError' || !/not a constructor/i.test(error.message) ) } } // setting prototype to null and converting to a string should throw a TypeError const getNullConversionTypeErrorLie = apiFunction => { const nativeProto = Object.getPrototypeOf(apiFunction) try { Object.setPrototypeOf(apiFunction, null) + '' return true } catch (error) { return error.constructor.name != 'TypeError' } finally { // restore proto Object.setPrototypeOf(apiFunction, nativeProto) } } // toString() and toString.toString() should return a native string in all frames const getToStringLie = (apiFunction, name, scope) => { /* Accepted strings: 'function name() { [native code] }' 'function name() {\n [native code]\n}' 'function get name() { [native code] }' 'function get name() {\n [native code]\n}' 'function () { [native code] }' `function () {\n [native code]\n}` */ let scopeToString, scopeToStringToString try { scopeToString = scope.Function.prototype.toString.call(apiFunction) } catch (e) { } try { scopeToStringToString = scope.Function.prototype.toString.call(apiFunction.toString) } catch (e) { } const apiFunctionToString = ( scopeToString ? scopeToString : apiFunction.toString() ) const apiFunctionToStringToString = ( scopeToStringToString ? scopeToStringToString : apiFunction.toString.toString() ) const trust = name => ({ [`function ${name}() { [native code] }`]: true, [`function get ${name}() { [native code] }`]: true, [`function () { [native code] }`]: true, [`function ${name}() {${'\n'} [native code]${'\n'}}`]: true, [`function get ${name}() {${'\n'} [native code]${'\n'}}`]: true, [`function () {${'\n'} [native code]${'\n'}}`]: true }) return ( !trust(name)[apiFunctionToString] || !trust('toString')[apiFunctionToStringToString] ) } // "prototype" in function should not exist const getPrototypeInFunctionLie = apiFunction => 'prototype' in apiFunction // "arguments", "caller", "prototype", "toString" should not exist in descriptor const getDescriptorLie = apiFunction => { const hasInvalidDescriptor = ( Object.getOwnPropertyDescriptor(apiFunction, 'arguments') || Reflect.getOwnPropertyDescriptor(apiFunction, 'arguments') || Object.getOwnPropertyDescriptor(apiFunction, 'caller') || Reflect.getOwnPropertyDescriptor(apiFunction, 'caller') || Object.getOwnPropertyDescriptor(apiFunction, 'prototype') || Reflect.getOwnPropertyDescriptor(apiFunction, 'prototype') || Object.getOwnPropertyDescriptor(apiFunction, 'toString') || Reflect.getOwnPropertyDescriptor(apiFunction, 'toString') ) return hasInvalidDescriptor } // "arguments", "caller", "prototype", "toString" should not exist as own property const getOwnPropertyLie = apiFunction => { const hasInvalidOwnProperty = ( apiFunction.hasOwnProperty('arguments') || apiFunction.hasOwnProperty('caller') || apiFunction.hasOwnProperty('prototype') || apiFunction.hasOwnProperty('toString') ) return hasInvalidOwnProperty } // descriptor keys should only contain "name" and "length" const getDescriptorKeysLie = apiFunction => { const descriptorKeys = Object.keys(Object.getOwnPropertyDescriptors(apiFunction)) const hasInvalidKeys = '' + descriptorKeys != 'length,name' && '' + descriptorKeys != 'name,length' return hasInvalidKeys } // own property names should only contain "name" and "length" const getOwnPropertyNamesLie = apiFunction => { const ownPropertyNames = Object.getOwnPropertyNames(apiFunction) const hasInvalidNames = !( '' + ownPropertyNames == 'length,name' || '' + ownPropertyNames == 'name,length' ) return hasInvalidNames } // own keys names should only contain "name" and "length" const getOwnKeysLie = apiFunction => { const ownKeys = Reflect.ownKeys(apiFunction) const hasInvalidKeys = !( '' + ownKeys == 'length,name' || '' + ownKeys == 'name,length' ) return hasInvalidKeys } // calling toString() on an object created from the function should throw a TypeError const getNewObjectToStringTypeErrorLie = apiFunction => { try { const you = () => Object.create(apiFunction).toString() const cant = () => you() const hide = () => cant() hide() // error must throw return true } catch (error) { const stackLines = error.stack.split('\n') const validScope = !/at Object\.apply/.test(stackLines[1]) // Stack must be valid const validStackSize = ( error.constructor.name == 'TypeError' && stackLines.length >= 5 ) // Chromium must throw error 'at Function.toString'... and not 'at Object.apply' if (validStackSize && IS_BLINK && ( !validScope || !/at Function\.toString/.test(stackLines[1]) || !/at you/.test(stackLines[2]) || !/at cant/.test(stackLines[3]) || !/at hide/.test(stackLines[4]) )) { return true } return !validStackSize } } /* Proxy Detection */ // arguments or caller should not throw 'incompatible Proxy' TypeError const tryIncompatibleProxy = fn => { try { fn() return true // failed to throw } catch (error) { return ( error.constructor.name != 'TypeError' || (IS_GECKO && /incompatible\sProxy/.test(error.message)) ) } } const getIncompatibleProxyTypeErrorLie = apiFunction => { return ( tryIncompatibleProxy(() => apiFunction.arguments) || tryIncompatibleProxy(() => apiFunction.caller) ) } const getToStringIncompatibleProxyTypeErrorLie = apiFunction => { return ( tryIncompatibleProxy(() => apiFunction.toString.arguments) || tryIncompatibleProxy(() => apiFunction.toString.caller) ) } // checking proxy instanceof proxy should throw a valid TypeError const getInstanceofCheckLie = apiFunction => { const proxy = new Proxy(apiFunction, {}) if (!IS_BLINK) { return false } const hasValidStack = (error, type = 'Function') => { const { message, name, stack } = error const validName = name == 'TypeError' const validMessage = message == `Function has non-object prototype 'undefined' in instanceof check` const targetStackLine = ((stack || '').split('\n') || [])[1] const validStackLine = ( targetStackLine.startsWith(` at ${type}.[Symbol.hasInstance]`) || targetStackLine.startsWith(' at [Symbol.hasInstance]') // Chrome 102 ) return validName && validMessage && validStackLine } try { proxy instanceof proxy return true // failed to throw } catch (error) { // expect Proxy.[Symbol.hasInstance] if (!hasValidStack(error, 'Proxy')) { return true } try { apiFunction instanceof apiFunction return true // failed to throw } catch (error) { // expect Function.[Symbol.hasInstance] return !hasValidStack(error) } } } // defining properties should not throw an error const getDefinePropertiesLie = (apiFunction) => { if (!IS_BLINK) { return false // chrome only test } try { Object.defineProperty(apiFunction, '', { configurable: true })+'' Reflect.deleteProperty(apiFunction, '') return false } catch (error) { return true // failed at Error } } // setPrototypeOf error tests const spawnError = (apiFunction, method) => { if (method == 'setPrototypeOf') { return Object.setPrototypeOf(apiFunction, Object.create(apiFunction)) + '' } else { apiFunction.__proto__ = apiFunction return apiFunction++ } } const hasValidError = error => { const { name, message } = error const hasRangeError = name == 'RangeError' const hasInternalError = name == 'InternalError' const chromeLie = IS_BLINK && ( message != `Maximum call stack size exceeded` || !hasRangeError ) const firefoxLie = IS_GECKO && ( message != `too much recursion` || !hasInternalError ) return (hasRangeError || hasInternalError) && !(chromeLie || firefoxLie) } const getTooMuchRecursionLie = ({ apiFunction, method = 'setPrototypeOf' }) => { const nativeProto = Object.getPrototypeOf(apiFunction) const proxy = new Proxy(apiFunction, {}) try { spawnError(proxy, method) return true // failed to throw } catch (error) { return !hasValidError(error) } finally { Object.setPrototypeOf(proxy, nativeProto) // restore } } const getChainCycleLie = ({ apiFunction, method = 'setPrototypeOf' }) => { const nativeProto = Object.getPrototypeOf(apiFunction) try { spawnError(apiFunction, method) return true // failed to throw } catch (error) { const { name, message, stack } = error const targetStackLine = ((stack || '').split('\n') || [])[1] const hasTypeError = name == 'TypeError' const chromeLie = IS_BLINK && ( message != `Cyclic __proto__ value` || ( method == '__proto__' && ( !targetStackLine.startsWith(` at set __proto__ (<anonymous>)`) && // Chrome ~117+ !targetStackLine.startsWith(` at set __proto__ [as __proto__]`) && // Chrome 102+ !targetStackLine.startsWith(` at Function.set __proto__ [as __proto__]`) ) ) ) const firefoxLie = IS_GECKO && ( message != `can't set prototype: it would cause a prototype chain cycle` ) if (!hasTypeError || chromeLie || firefoxLie) { return true // failed Error } } finally { Object.setPrototypeOf(apiFunction, nativeProto) // restore } } const getReflectSetProtoLie = ({ apiFunction, randomId }) => { if (!randomId) { randomId = getRandomValues() } const nativeProto = Object.getPrototypeOf(apiFunction) try { if (Reflect.setPrototypeOf(apiFunction, Object.create(apiFunction))) { return true // failed value (expected false) } else { try { randomId in apiFunction return false } catch (error) { return true // failed at Error } } } catch (error) { return true // failed at Error } finally { Object.setPrototypeOf(apiFunction, nativeProto) // restore } } const getReflectSetProtoProxyLie = ({ apiFunction, randomId }) => { if (!randomId) { randomId = getRandomValues() } const nativeProto = Object.getPrototypeOf(apiFunction) const proxy = new Proxy(apiFunction, {}) try { if (!Reflect.setPrototypeOf(proxy, Object.create(proxy))) { return true // failed value (expected true) } else { try { randomId in apiFunction return true // failed to throw } catch (error) { return !hasValidError(error) } } } catch (error) { return true // failed at Error } finally { Object.setPrototypeOf(proxy, nativeProto) // restore } } // API Function Test const getLies = ({ apiFunction, proto, obj = null, lieProps }) => { if ('function' != typeof apiFunction) { return { lied: false, lieTypes: [] } } const name = apiFunction.name.replace(/get\s/, '') let lies = { // custom lie string names [`a: failed illegal error`]: obj ? getIllegalTypeErrorLie(obj, name) : false, [`b: failed undefined properties`]: obj ? getUndefinedValueLie(obj, name) : false, [`c: failed call interface error`]: getCallInterfaceTypeErrorLie(apiFunction, proto), [`d: failed apply interface error`]: getApplyInterfaceTypeErrorLie(apiFunction, proto), [`e: failed new instance error`]: getNewInstanceTypeErrorLie(apiFunction), [`f: failed class extends error`]: getClassExtendsTypeErrorLie(apiFunction), [`g: failed null conversion error`]: getNullConversionTypeErrorLie(apiFunction), [`h: failed toString`]: getToStringLie(apiFunction, name, scope), [`i: failed "prototype" in function`]: getPrototypeInFunctionLie(apiFunction), [`j: failed descriptor`]: getDescriptorLie(apiFunction), [`k: failed own property`]: getOwnPropertyLie(apiFunction), [`l: failed descriptor keys`]: getDescriptorKeysLie(apiFunction), [`m: failed own property names`]: getOwnPropertyNamesLie(apiFunction), [`n: failed own keys names`]: getOwnKeysLie(apiFunction), [`o: failed object toString error`]: getNewObjectToStringTypeErrorLie(apiFunction), // Proxy Detection [`p: failed at incompatible proxy error`]: getIncompatibleProxyTypeErrorLie(apiFunction), [`q: failed at toString incompatible proxy error`]: getToStringIncompatibleProxyTypeErrorLie(apiFunction), [`r: failed at too much recursion error`]: getChainCycleLie({ apiFunction }) } // conditionally use advanced detection const detectProxies = ( name == 'toString' || !!lieProps['Function.toString'] ) if (detectProxies) { lies = Object.assign( {}, lies, // Advanced Proxy Detection { [`s: failed at too much recursion __proto__ error`]: getChainCycleLie({ apiFunction, method: '__proto__' }), [`t: failed at chain cycle error`]: getTooMuchRecursionLie({ apiFunction }), [`u: failed at chain cycle __proto__ error`]: getTooMuchRecursionLie({ apiFunction, method: '__proto__' }), [`v: failed at reflect set proto`]: getReflectSetProtoLie({ apiFunction, randomId }), [`w: failed at reflect set proto proxy`]: getReflectSetProtoProxyLie({ apiFunction, randomId }), [`x: failed at instanceof check error`]: getInstanceofCheckLie(apiFunction), [`y: failed at define properties`]: getDefinePropertiesLie(apiFunction) } ) } const lieTypes = Object.keys(lies).filter(key => !!lies[key]) return { lied: lieTypes.length, lieTypes } } // Lie Detector const createLieDetector = () => { const isSupported = obj => typeof obj != 'undefined' && !!obj const props = {} // lie list and detail let propsSearched = [] // list of properties searched return { getProps: () => props, getPropsSearched: () => propsSearched, searchLies: (fn, { target = [], ignore = [] } = {}) => { let obj // check if api is blocked or not supported try { obj = fn() if (!isSupported(obj)) { return } } catch (error) { return } const interfaceObject = !!obj.prototype ? obj.prototype : obj ;[...new Set([ ...Object.getOwnPropertyNames(interfaceObject), ...Object.keys(interfaceObject) // backup ])].sort().forEach(name => { const skip = ( name == 'constructor' || (target.length && !new Set(target).has(name)) || (ignore.length && new Set(ignore).has(name)) ) if (skip) { return } const objectNameString = /\s(.+)\]/ const apiName = `${ obj.name ? obj.name : objectNameString.test(obj) ? objectNameString.exec(obj)[1] : undefined }.${name}` propsSearched.push(apiName) try { const proto = obj.prototype ? obj.prototype : obj let res // response from getLies // search if function try { const apiFunction = proto[name] // may trigger TypeError if ('function' == typeof apiFunction) { res = getLies({ apiFunction: proto[name], proto, lieProps: props }) if (res.lied) { return (props[apiName] = res.lieTypes) } return } // since there is no TypeError and the typeof is not a function, // handle invalid values and ignore name, length, and constants if ( name != 'name' && name != 'length' && name[0] !== name[0].toUpperCase()) { const lie = [`z: failed descriptor.value undefined`] return ( props[apiName] = lie ) } } catch (error) { } // else search getter function const getterFunction = Object.getOwnPropertyDescriptor(proto, name).get res = getLies({ apiFunction: getterFunction, proto, obj, lieProps: props }) // send the obj for special tests if (res.lied) { return (props[apiName] = res.lieTypes) } return } catch (error) { const lie = `aa: failed prototype test execution` return ( props[apiName] = [lie] ) } }) } } } const lieDetector = createLieDetector() const { searchLies } = lieDetector // search lies: remove target to search all properties // test Function.toString first to determine the depth of the search searchLies(() => Function, { target: [ 'toString', ], ignore: [ 'caller', 'arguments' ] }) // other APIs searchLies(() => AnalyserNode) searchLies(() => AudioBuffer, { target: [ 'copyFromChannel', 'getChannelData' ] }) searchLies(() => BiquadFilterNode, { target: [ 'getFrequencyResponse' ] }) searchLies(() => CanvasRenderingContext2D, { target: [ 'getImageData', 'getLineDash', 'isPointInPath', 'isPointInStroke', 'measureText', 'quadraticCurveTo', 'font' ] }) searchLies(() => CSSStyleDeclaration, { target: [ 'removeProperty', 'setProperty' ] }) searchLies(() => CSS2Properties, { // Gecko 143 or lower target: [ 'setProperty' ] }) searchLies(() => CSSStyleProperties, { // Gecko 144+ target: [ 'setProperty' ] }) searchLies(() => Date, { target: [ 'getDate', 'getDay', 'getFullYear', 'getHours', 'getMinutes', 'getMonth', 'getTime', 'getTimezoneOffset', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 'setMonth', 'setSeconds', 'setTime', 'toDateString', 'toJSON', 'toLocaleDateString', 'toLocaleString', 'toLocaleTimeString', 'toString', 'toTimeString', 'valueOf' ] }) searchLies(() => Intl.DateTimeFormat, { target: [ 'format', 'formatRange', 'formatToParts', 'resolvedOptions' ] }) searchLies(() => Document, { target: [ 'adoptedStyleSheets', 'createElement', 'createElementNS', 'getElementById', 'getElementsByClassName', 'getElementsByName', 'getElementsByTagName', 'getElementsByTagNameNS', 'referrer', 'styleSheets', 'write', 'writeln' ], ignore: [ // Firefox returns undefined on getIllegalTypeErrorLie test 'onreadystatechange', 'onmouseenter', 'onmouseleave' ] }) searchLies(() => DOMRect) searchLies(() => DOMRectReadOnly) searchLies(() => Element, { target: [ 'append', 'appendChild', 'attachShadow', 'getBoundingClientRect', 'getClientRects', 'insertAdjacentElement', 'insertAdjacentHTML', 'insertAdjacentText', 'insertBefore', 'prepend', 'replaceChild', 'replaceWith', 'setAttribute' ] }) searchLies(() => FontFace, { target: [ 'family', 'load', 'status' ] }) searchLies(() => HTMLCanvasElement) searchLies(() => HTMLElement, { target: [ 'clientHeight', 'clientWidth', 'offsetHeight', 'offsetWidth', 'scrollHeight', 'scrollWidth' ], ignore: [ // Firefox returns undefined on getIllegalTypeErrorLie test 'onmouseenter', 'onmouseleave' ] }) searchLies(() => HTMLIFrameElement, { target: [ 'contentDocument', 'contentWindow', ] }) searchLies(() => IntersectionObserverEntry, { target: [ 'boundingClientRect', 'intersectionRect', 'rootBounds' ] }) searchLies(() => Math, { target: [ 'acos', 'acosh', 'asinh', 'atan', 'atan2', 'atanh', 'cbrt', 'cos', 'cosh', 'exp', 'expm1', 'log', 'log10', 'log1p', 'pow', 'sin', 'sinh', 'sqrt', 'tan', 'tanh' ] }) searchLies(() => MediaDevices, { target: [ 'enumerateDevices', 'getDisplayMedia', 'getUserMedia' ] }) searchLies(() => Navigator, { target: [ 'appCodeName', 'appName', 'appVersion', 'buildID', 'connection', 'deviceMemory', 'getBattery', 'getGamepads', 'getVRDisplays', 'globalPrivacyControl', 'hardwareConcurrency', 'language', 'languages', 'maxTouchPoints', 'mimeTypes', 'oscpu', 'pdfViewerEnabled', 'platform', 'plugins', 'product', 'productSub', 'sendBeacon', 'serviceWorker', 'userAgent', 'userAgentData', 'vendor', 'vendorSub', 'webdriver', ] }) searchLies(() => Node, { target: [ 'appendChild', 'insertBefore', 'replaceChild' ] }) searchLies(() => OffscreenCanvas, { target: [ 'convertToBlob', 'getContext' ] }) searchLies(() => OffscreenCanvasRenderingContext2D, { target: [ 'getImageData', 'getLineDash', 'isPointInPath', 'isPointInStroke', 'measureText', 'quadraticCurveTo', 'font' ] }) searchLies(() => Range, { target: [ 'getBoundingClientRect', 'getClientRects', ] }) searchLies(() => Intl.RelativeTimeFormat, { target: [ 'resolvedOptions' ] }) searchLies(() => Screen) searchLies(() => speechSynthesis, { target: [ 'getVoices' ] }) searchLies(() => StorageManager, { target: [ 'estimate', ] }) searchLies(() => String, { target: [ 'fromCodePoint' ] }) searchLies(() => SVGRect) searchLies(() => TextMetrics) searchLies(() => WebGLRenderingContext, { target: [ 'bufferData', 'getParameter', 'readPixels' ] }) searchLies(() => WebGL2RenderingContext, { target: [ 'bufferData', 'getParameter', 'readPixels' ] }) /* potential targets: RTCPeerConnection Plugin PluginArray MimeType MimeTypeArray Worker History */ // disregard Function.prototype.toString lies to filter direct API tampering const getCountOfNonFunctionToStringLies = x => !x ? x : x.filter(x => !/o:|q:/.test(x)).length // return lies list and detail const props = lieDetector.getProps() const propsSearched = lieDetector.getPropsSearched() return { lieList: Object.keys(props).sort(), lieDetail: props, lieCount: Object.keys(props).reduce((acc, key) => acc + props[key].length, 0), propsSearched, // filter out lies on Function.prototype.toString tamperingList: (props => { return Object.keys(props).filter(key => { const totalTamperingLies = getCountOfNonFunctionToStringLies(props[key]) if (!totalTamperingLies) { return false } return true }) })(props) } } // start const { lieList, lieDetail, lieCount, propsSearched, tamperingList } = getPrototypeLies(iframeWindow) // execute and destructure the list and detail if (iframeContainerDiv) { iframeContainerDiv.parentNode.removeChild(iframeContainerDiv) } // navigator // prototype items that are not navigator // this is lie 'b: failed undefined properties'? but here we cover all navigator keys try { let aNav = [] for (const key in navigator) {aNav.push(key)} let aProto = Object.keys(Object.getOwnPropertyDescriptors(Navigator.prototype)) aProto = aProto.filter(x => !['constructor'].includes(x)) // ignore constructor let aNotInNav = aProto.filter(x => !aNav.includes(x)) // //let aNotInProto = aNav.filter(x => !aProto.includes(x)) // this would catch made up shit? //console.log(aNav, aProto, aNotInNav, aNotInProto) aNotInNav.forEach(function(item) { item = 'Navigator.'+ item if (lieDetail[item] == undefined) {lieDetail[item] = []} lieDetail[item].push('zz: failed getOwnPropertyDescriptors') if (!tamperingList.includes(item)) {tamperingList.push(item)} }) } catch(e) {console.log(e)} // sData sData[SECT98] = {} for (const k of Object.keys(lieDetail).sort()) {sData[SECT98][k] = lieDetail[k]} sData[SECT99] = tamperingList.sort() if (!gRun) {return resolve()} // gData gData[SECT99] = sData[SECT99] gData[SECT98] = {} if (Object.keys(sData[SECT98]).length) { let newObj = {} for (const k of Object.keys(sData[SECT98])) {newObj[k] = sData[SECT98][k]} gData[SECT98] = newObj } gData[SECT97] = propsSearched.sort() log_perf(SECTP, SECT98 +"/"+ SECT99, t0) return resolve() }) countJS(SECTP) ================================================ FILE: js/region.js ================================================ 'use strict'; /* HEADERS */ function get_nav_connection(METRIC) { let hash, btn ='', data ='', notation = default_red try { hash = navigator.connection if (runST) {hash = null} else if (runSI) {hash = {}} else if (runSL) {addProxyLie('Navigator.'+ METRIC)} if (undefined === hash) { hash = hash+''; notation = default_green } else { let typeCheck = typeFn(hash, true) if ('object' !== typeCheck) {throw zErrType + typeFn(hash)} let expected = '[object NetworkInformation]' if (hash+'' !== expected) {throw zErrInvalid + 'expected '+ expected +': got '+ hash} // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation let keyTypes = { // gecko only addEventListener: 'function', dispatchEvent: 'function', ontypechange: 'null', removeEventListener: 'function', type: 'string', // also in blink downlink: 'number', downlinkMax: 'null', effectiveType: 'string', onchange: 'null', rtt: 'number', saveData: 'boolean', when: 'function', } let oGood = { effectiveType: ['slow-2g','2g','3g','4g'], type: ['bluetooth','cellular','ethernet','none','wifi','wimax','other','unknown'] } let oTemp = {} for (let k in hash) { try { let x = navigator.connection[k] if (runSI) {x = undefined} // type check let typeCheck = typeFn(x), expectedType = keyTypes[k] if (typeCheck !== expectedType) { let isInvalid = true // https://groups.google.com/a/chromium.org/g/blink-dev/c/tU_Hqqytx8g/m/HTJebzVHBAAJ // "WiFi on Android reports Infinity for downlinkMax as Chrome recently dropped the required permission to get Wifi linkSpeed if ('blink' == isEngine && 'downlinkMax' == k && 'Infinity' == typeCheck) {isInvalid = false} if (isInvalid) {throw zErrInvalid +'expected '+ expectedType +': got '+ typeCheck} } // valid string if ('type' == k || 'effectiveType' == k) { if (runSI) {x = '1g'} let aGood = oGood[k] if (!aGood.includes(x)) {throw zErrInvalid + ': got ' + x} if ('slow-2g' == x) {x = '2g'} // treat slow-2g as 2g } // cleanup if ('function' === typeCheck) {x = typeCheck} if (null == x || Infinity == x) {x += ''} // record null/Infinity as strings | note: 'null'/'Infinity' are caught as errors // stability if ('rtt' == k) {x = zNA} else if ('downlink' == k) { x = Math.floor(x)} oTemp[k] = x } catch(e) { oTemp[k] = zErr log_error(5, METRIC +'_'+ k, e) } } data = {} for (const k of Object.keys(oTemp).sort()) {data[k] = oTemp[k]} hash = mini(data); btn = addButton(5, METRIC) } } catch(e) { hash = e; data = zErrLog } addBoth(5, METRIC, hash, btn, notation, data, isProxyLie('Navigator.'+ METRIC)) return } function get_nav_dnt(METRIC) { // ignored // navigator.msDoNotTrack = IE9 + 10 // window.doNotTrack = IE11 and Edge 16- and old Safari // gecko: this is an expected property // nonGecko // blink: string vs null: i.e a string of "null" will be an error // webkit: undefined vs null: i.e a string of "undefined" will be an error let hash, data ='', expectedType = isGecko ? 'string' : 'undefined' try { hash = navigator[METRIC] if (runST) {hash = 1} else if (runSI) {hash = '2'} let typeCheck = typeFn(hash) if ('blink' == isEngine) { // blink can be pnly be "1" or null if ('1' !== hash && null !== hash) {throw zErrInvalid + 'expected 1 or null: got ' + hash} } else { if (expectedType !== typeCheck) {throw zErrType + typeCheck} if (isGecko) { if ('1' !== hash && 'unspecified' !== hash) {throw zErrInvalid + 'expected 1 or unspecified: got ' + hash} } } hash += '' // gecko is a string, otherwise we can only be null/undefined, so convert to a string } catch(e) { hash = e; data = zErrLog } addBoth(5, METRIC, hash,'','', data) return } function get_nav_online(METRIC) { // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine let value, data ='', notation = rfp_red try { value = navigator.onLine if (runST) {value = undefined} let typeCheck = typeFn(value) // we expect blink, gecko, webkit to return a boolean if ('boolean' !== typeCheck) {throw zErrType + typeCheck} if (value) {notation = rfp_green} // 1975851: FF142+ } catch(e) { value = e; data = zErrLog } addBoth(5, METRIC, value,'', notation, data) return } function get_nav_gpc(METRIC) { // GPC: 1670058 // privacy.globalprivacycontrol.functionality.enabled = navigator // privacy.globalprivacycontrol.enabled = true/false // FF120+ desktop (?android): gpc enabled: false but true in pb mode // ToDo: FF144+? 1983296 functionality pref deprecated let hash, data ='', notation = isBB ? default_red : '' try { hash = navigator[METRIC] if (runST) {hash = null} else if (runSL) {addProxyLie('Navigator.'+ METRIC)} if (undefined === hash) { hash = hash+'' } else { let typeCheck = typeFn(hash) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} // expected boolean but could be true or false, so don't notate // except BB where we expect true due to pb mode if (isBB && true === hash) {notation = default_green} } } catch(e) { hash = e; data = zErrLog } addBoth(5, METRIC, hash,'', notation, data, isProxyLie('Navigator.'+ METRIC)) return } /* REGION */ function add_microperf_intl(m, countC, tsub0, isIntl) { if (undefined == oIntlLocalePerf[m]) {oIntlLocalePerf[m] = {}} if (isIntl) {oIntlLocalePerf[m]['constructors'] = countC} let subname = (isIntl ? 'intl' : 'string') oIntlLocalePerf[m][subname] = nowFn() - tsub0 } function set_isLanguageSmart() { // set once: ignore android for now if (!gLoad || !isSmart && !isSmartDataMode || !isDesktop) {return} // BB if ESR // resource://gre/res/multilocale.txt isLanguageSmart = isBB const en = 'en-US, en' languagesSupported = { // language = existing key | languages = key + value[0] | locale = key unless value[1] !== undefined 'ar': [en], 'be': [en], 'bg': [en], 'ca': [en], 'cs': ['sk, '+ en], 'da': [en], 'de': [en], 'el-GR': ['el, '+ en, 'el'], 'en-US': ['en'], 'es-ES': ['es, '+ en], 'fa-IR': ['fa, '+ en, 'fa'], 'fi-FI': ['fi, '+ en, 'fi'], 'fr': ['fr-FR, '+ en], 'ga-IE': ['ga, en-IE, en-GB, '+ en], 'he': ['he-IL, '+ en], 'hu-HU': ['hu, '+ en, 'hu'], 'id': [en], 'is': [en], 'it-IT': ['it, '+ en, 'it'], 'ja': [en], 'ka-GE': ['ka, '+ en, 'ka'], 'ko-KR': ['ko, '+ en, 'ko'], 'lt': [en +', ru, pl'], 'mk-MK': ['mk, '+ en, 'mk'], 'ms': [en], 'my': ['en-GB, en'], 'nb-NO': ['nb, no-NO, no, nn-NO, nn, '+ en], 'nl': [en], 'pl': [en], 'pt-BR': ['pt, '+ en], 'pt-PT': ['pt, en, en-US'], 'ro-RO': ['ro, en-US, en-GB, en', 'ro'], 'ru-RU': ['ru, '+ en, 'ru'], 'sq': ['sq-AL, '+ en], 'sv-SE': ['sv, '+ en], 'th': [en], 'tr-TR': ['tr, '+ en, 'tr'], 'uk-UA': ['uk, '+ en, 'uk'], 'vi-VN': ['vi, '+ en, 'vi'], 'zh-CN': ['zh, zh-TW, zh-HK, '+ en, 'zh-Hans-CN'], 'zh-TW': ['zh, '+ en, 'zh-Hant-TW'], } // these are current stable BB hashes since last checked // note: upstream ESR seems to pick up stable l10n changes // last checked TB15.06 // NOTE: these hashes are only designed to work with BB ESR (stable) and FF en-US // we can use an array if necessary so as to not get false positives in FF // using an array (test is .includes) means we're not super tight on our health check i.e per isVer let xsEN = '6cc5a8b4' localesSupported = { // v hashes are with localized NumberRangeOver/Underflow // c = css | m = media | v = verification | x = xml | xs = xslt | xsort = xslt sort // r = reporting (if blank we use the english hash) 'ar': { m: '1f9a06e3', v: '7262bcc6', x: '71982b47', xs: '5cee96ec', xsort: '352c4e34', r: ''}, 'be': { m: '076d68e6', v: '4edeafab', x: '42583d22', xs: 'c28dba41', xsort: '74053574', r: '6de2f0b7'}, 'bg': { m: '2da6c02e', v: 'ce892c88', x: 'c4f06f98', xs: 'b964cfe0', xsort: '7d747674', r: ''}, 'ca': { m: 'd856d812', v: '6b3bb3d8', x: '77a62a49', xs: 'ad2e7060', xsort: xsEN, r: ''}, 'cs': { m: 'c92accb0', v: 'de3ab0ad', x: '81c91d49', xs: '7c010d86', xsort: 'a7ddfef4', r: '39eac55d'}, 'da': { m: '39169214', v: '479797a1', x: 'a30818e8', xs: '8654b0f1', xsort: '88f55cfa', r: '266a324b'}, 'de': { m: '298d11c6', v: 'f9e2eae6', x: 'c1ce6571', xs: '5ab0cbb9', xsort: xsEN, r: '8c11ce07'}, 'el': { m: '39712e09', v: 'fb391308', x: '493f7225', xs: '33a4584c', xsort: 'cae41bf4', r: '71191fa1'}, 'en-US': {m: '05c30936', v: '41310558', x: '544e1ae8', xs: 'bcb04adc', xsort: xsEN, r: ['8c954475','4a9afc22']}, 'es-ES': {m: '96b78cbd', v: '97c3f5a9', x: 'ed807f70', xs: 'd9a6e947', xsort: '32fce55a', r: '7fc42e10'}, 'fa': { m: '6648d919', v: '8ef57409', x: '1ed34bca', xs: '47876cea', xsort: 'ff0f7334', r: ''}, 'fi': { m: '82d079c7', v: '3e29e6e7', x: '859efc32', xs: '67b222db', xsort: '26f7a3f8', r: ''}, 'fr': { m: '024d0fce', v: '34e28fa2', x: '1d2050d3', xs: 'f09eacaa', xsort: xsEN, r: '7ebbf4b3'}, 'ga-IE': {m: '97fca229', v: '2bf1321d', x: 'd3af2cd8', xs: '021b6b57', xsort: xsEN, r: ''}, 'he': { m: 'cdde832b', v: 'e47dbb82', x: 'c7274a3e', xs: '35d1f35c', xsort: 'a0fcc2b4', r: 'cadeed05'}, 'hu': { m: 'db7366e6', v: 'b72d316d', x: 'e4f85168', xs: 'ffae360e', xsort: '2fe650b4', r: '4b0d44d0'}, 'id': { m: '1e275882', v: '5dda18f3', x: 'a70cd23c', xs: '26e6e4fb', xsort: xsEN, r: '93c32eba'}, 'is': { m: '204c8f73', v: '6bbe7a8f', x: 'edb8b212', xs: '3d227a5a', xsort: '93b575f8', r: 'ce6ce0a6'}, 'it': { m: '716e7242', v: '3b781f09', x: 'c567f479', xs: '7d0eba5c', xsort: xsEN, r: '514ebfe9'}, 'ja': { m: 'ab56d7cb', v: '48645d06', x: 'a58f6165', xs: 'a0fa98ad', xsort: '22ec9486', r: '64e8a6a1'}, 'ka': { m: '6961b7e4', v: '40feb44f', x: '765afcb4', xs: '460ae32f', xsort: '7a65b6b4', r: '778bc94a'}, 'ko': { m: 'c758b027', v: 'd3b54047', x: '1235e26d', xs: '1d314216', xsort: '9c39494c', r: 'c81a1027'}, 'lt': { m: 'c36fbafb', v: 'd5f9b95d', x: 'b0e8a3bc', xs: 'ca28b814', xsort: 'f26c6ff4', r: 'e58fc47e'}, 'mk': { m: '78274f1b', v: '333aae58', x: 'b6020ec1', xs: '36e30ccb', xsort: 'f9e81474', r: ''}, 'ms': { m: '3e26c6be', v: '9dadbc64', x: '15e6148f', xs: '421d606a', xsort: xsEN, r: '411351e5'}, 'my': { m: '939f2013', v: '43cc3aa3', x: 'a6571ec7', xs: 'bfc734fe', xsort: 'fbfb1d8c', r: ''}, 'nb-NO': {m: '1d496fea', v: '84ce54eb', x: 'e0d34e04', xs: '19e8e2a5', xsort: '88f55cfa', r: 'b32738cf'}, 'nl': { m: 'e1d3b281', v: '326cbfd2', x: 'caef95fc', xs: '8a47ae1a', xsort: xsEN, r: '2a725fb7'}, 'pl': { m: '0bd88e98', v: '95ad4851', x: '2a45177d', xs: '4740c17a', xsort: '01902794', r: '2678c528'}, 'pt-BR': {m: '39835e93', v: 'de2c3569', x: '68f80c66', xs: 'e710618b', xsort: xsEN, r: '21ee14c6'}, 'pt-PT': {m: '6ae9a13a', v: 'b21f3984', x: '0aa2a309', xs: '025ca23b', xsort: xsEN, r: 'e6a7d6ff'}, 'ro': { m: '3e321768', v: 'd72a350b', x: 'a9da3416', xs: '61b5e498', xsort: '2a01a4d8', r: '9b675c63'}, 'ru': { m: '8e9b7945', v: '2391fbec', x: '26f663da', xs: '4445d36a', xsort: '7d747674', r: '0bf2516d'}, 'sq': { m: '91943e67', v: 'e0259277', x: '4e0bbdcd', xs: '569be7bb', xsort: 'f45c6af8', r: 'a75d2c6f'}, 'sv-SE': {m: 'bc792ce2', v: 'd9d7828b', x: '4af3452f', xs: '701cd8c7', xsort: '1ca25322', r: '3ed80374'}, 'th': { m: 'a32d70a7', v: '07358a87', x: '2a04071a', xs: '7e968207', xsort: 'a0bff3b4', r: '65ade427'}, 'tr': { m: '4217ef80', v: '5048d312', x: '55daef93', xs: 'd8e92945', xsort: 'e9fda72a', r: 'd62d2c72'}, 'uk': { m: '4bea2a13', v: '0163f51d', x: '4f817ea3', xs: 'e62ccf4f', xsort: 'ae65fe74', r: '2049852a'}, 'vi': { m: 'bba6c980', v: 'b8137d59', x: '80da1efb', xs: '959b2e31', xsort: '2a01a4d8', r: 'ef6841d7'}, 'zh-Hans-CN': {m: '550ea53e', v: '0e58f82a', x: '536abb21', xs: '1feed45e', xsort: '42d5bac6', r: '135f1290'}, 'zh-Hant-TW': {m: '66b515a4', v: '8e4cfa0e', x: '9ad3338c', xs: '8aa6bfbf', xsort: '6d106412', r: '62cefab7'}, } // mac: japanese languages are the same but the locale is 'ja-JP' not 'ja' if ('mac' == isOS) { languagesSupported['ja'].push('ja-JP') localesSupported['ja-JP'] = localesSupported.ja delete localesSupported['ja'] } if (isMB) { // 22 of 38 supported let notSupported = [ // lang 'be','bg','ca','cs','el-GR','ga-IE','he','hu-HU','id','is','ka-GE','lt','mk-MK','ms','pt-PT','ro-RO','sq','uk-UA','vi-VN', // + locales 'el','hu','ka','mk','ro','uk','vi', ] notSupported.forEach(function(key){ delete languagesSupported[key] delete localesSupported[key] }) } return } function set_oIntlDate() { let d = oIntlDates oIntlDate = { date_timestyle : { "default": { 'full_medium': [d.JanA, d.JanB, d.JulA, d.JulB, d.SepA, d.SepB], 'medium_long': [d.JanA, d.JanB, d.JulA, d.JulB, d.NovA, d.NovB], 'short_full': [d.SepA, d.SepB], }, 'ethiopic': {'full': [d.JanA], 'medium': [d.JanA]}, 'japanese': {'medium': [d.SepA, d.NovA]}, // NovA required for blink (147) }, } // build keys for (const k of Object.keys(oIntlDate)) { oIntlDateKeys[k] = [] for (const j of Object.keys(oIntlDate[k]).sort()) {oIntlDateKeys[k].push(j)} } } function set_oIntlDates() { /* intl.dates all dates (days/months/am-pm) must account for timezones - that way everyone covers the specific targets (e.g. am, friday, single digit day, etc) - timezone entropy is in the actual timezonename (we're confirming that here) we use UTC so we can check the original date hasn't been altered - which means we will nd up testing more dates to cover specific days - at this point AM/PM doesn't seem to be a factor - this makes the PoC's max entropy easier to verify timezones can be 14 hrs less or 12 hrs more but (IIUIC) our selected dates aren't hiting those instances where it exceeds ±12 (or I lucked out) and we end up only needing two identical times on subsequent days tests/PoCs need to cover all possible combos of locales x timezonenames because those are the TWO variables that I cannot control (oIntlLocale only have ONE variable: locale) and not all locales handle timezonenames to the same degree: e.g. - America/Los_Angles has 343 possible outcomes, Europe/Vatican has 344: this is because - pt-ao + pt-ch vary for the vatican but not for los angeles - tl;dr: locale + timezonename PoCs cover a range intl.locale all dates (days/months/am-pm) must be timezone resistent: we are checking locale only - reported timezonename (and locale) is tested see oIntlDate section - thus we use UTC time so everyone uses the exact same dates, and then we pass UTC as the timezone so nothing shifts, preserving our specific datetimes the tests that expose day/time are datetimeformat's relatedYear + components + timezonename | and dayperiods */ oIntlDates = { //intl.dates JanA: new Date('2024-01-04T04:12:34.000Z'), JanB: new Date('2024-01-05T04:12:34.000Z'), JulA: new Date('2024-07-04T14:12:34.000Z'), JulB: new Date('2024-07-05T14:12:34.000Z'), SepA: new Date('2024-09-03T04:12:34.000Z'), SepB: new Date('2024-09-04T04:12:34.000Z'), NovA: new Date('2024-11-03T14:12:34.000Z'), NovB: new Date('2024-11-04T14:12:34.000Z'), //intl.locale // fractionalSecondDigits: we only ever reveal the seconds FSD: new Date('2023-06-10T01:12:34.567Z'), // month (x4) + year (xJan): we only ever reveal the month or year Jan: new Date('2023-01-15T00:00:00.000Z'), Jun: new Date('2023-06-15T00:00:00.000Z'), Sep: new Date('2023-09-15T00:00:00.000Z'), Nov: new Date('2023-11-15T00:00:00.000Z'), // days (x2) + hrs (xFri) + era: expose day/hr Wed: new Date('2023-01-18T01:00:00.000Z'), // doubles as hour 1 Fri: new Date('2023-01-20T13:00:00.000Z'), // doubles as hour 13 Era: new Date('-000002-01-15T01:00:00.000Z'), // relatedyear exposes day RY1: new Date('-000002-01-15T01:00:00.000Z'), RY2: new Date('2023-01-15T00:00:00.000Z'), // timezonename exposes day but we pass the timezone itself so it's relative (i.e stable per timezone) TZN1: new Date('2019-08-15T00:00:00.000Z'), // dayperiod: exposes hr DP8: new Date('2019-01-30T08:00:00Z'), DP12: new Date('2019-01-30T12:00:00Z'), DP15: new Date('2019-01-30T15:00:00Z'), DP18: new Date('2019-01-30T18:00:00Z'), DP22: new Date('2019-01-30T22:00:00Z'), } } function set_oIntlLocale() { let d = oIntlDates let tzLG = {'longGeneric': [d.TZN1]}, tzSG = {'shortGeneric': [d.TZN1]} let unitN = {'narrow': [1]}, unitL = {'long': [1]}, unitB = {'long': [1], 'narrow': [1]} let curAN = {"accounting": [-1000], "name": [-1]}, curN = {"name": [-1]}, curS = {"symbol": [1000]} oIntlLocale = { collation: { search: ['\u0107','\u0109','\u1ED9','\u00F6'], sort: [ 'A','a','aa','ch','ez','kz','ng','ph','ts','tt','y','\u00E2','\u00E4','\u00E7\a','\u00EB','\u00ED','\u00EE','\u00F0', '\u00F1','\u00F6','\u0107','\u0109','\u0137\a','\u0144','\u0149','\u01FB','\u025B','\u03B1','\u040E','\u0439','\u0453', '\u0457','\u04F0','\u0503','\u0561','\u05EA','\u0627','\u0649','\u06C6','\u06C7','\u06CC','\u06FD','\u0934','\u0935', '\u09A4','\u09CE','\u0A85','\u0B05','\u0B85','\u0C05','\u0C85','\u0D85','\u0E24','\u0E9A','\u10350','\u10D0','\u1208', '\u1780','\u1820','\u1D95','\u1DD9','\u1ED9','\u1EE3','\u311A','\u3147','\u4E2D','\uA647','\uFB4A' ] }, // DTF 'datetimeformat.components': { era: { // we need to control the date part so toLocaleString matches 'long': [{era: 'long', year: 'numeric', month: 'numeric', day: 'numeric'}, [d.Era]] }, fractionalSecondDigits: { '1': [{minute: 'numeric', second: 'numeric', fractionalSecondDigits: 1}, [d.FSD]] }, hour: { 'numeric': [{hour: 'numeric'}, [d.Wed]], }, hourCycle: { 'h11-2-digit': [{hour: '2-digit', hourCycle: 'h11'}, [d.Wed]] }, month: { 'narrow': [{month: 'narrow'}, [d.Nov] ], 'short': [{month: 'short'}, [d.Jan, d.Jun, d.Sep, d.Nov]], }, weekday: { 'long': [{weekday: 'long'}, [d.Wed, d.Fri]], 'narrow': [{weekday: 'narrow'}, [d.Wed, d.Fri]], 'short': [{weekday: 'short'}, [d.Fri]], }, year: { '2-digit': [{year: "2-digit"}, [d.Jan]] }, }, 'datetimeformat.dayperiod': { 'long': [d.DP8, d.DP22], 'narrow': [d.DP8, d.DP15], 'short': [d.DP12, d.DP15, d.DP18] }, 'datetimeformat.listformat': { 'narrow': ['conjunction','disjunction','unit'], 'short': ['unit'], 'long': ['conjunction','unit'] }, 'datetimeformat.relatedyear': { // these are all long buddhist: [d.RY1], chinese: [d.RY1], coptic: [d.RY2], 'default': [d.RY1], gregory: [d.RY1], hebrew: [d.RY1], indian: [d.RY1], 'islamic-tbla': [d.RY1], japanese: [d.RY1, d.RY2], roc: [d.RY1], }, 'datetimeformat.timezonename': { 'Africa/Douala': tzLG, 'America/Montevideo': tzSG, 'America/Winnipeg': tzLG, 'Asia/Hong_Kong': tzSG, 'Asia/Seoul': tzLG, 'Europe/London': tzSG, 'Asia/Muscat': tzSG, }, // DN displaynames: { calendar: { 'short': ['chinese','dangi','ethiopic','gregory','islamic-tbla','islamic-umalqura','japanese','roc'], }, currency: {'long': ['JPY','NIO','SEK','SZL','TZS','XAF']}, datetimefield: { 'narrow': ['day','dayPeriod','weekOfYear','weekday'], 'short': ['era','month','second','timeZoneName'], }, language: {'dialect': ['bn-in','en','fr-ch','gu','kl','sr-ba','zh-hk']}, region: {'narrow': ['CM','FR','TL','US','VC','VI','ZZ']}, script: { // blink is case sensitive 'short': ['Arab','Beng','Cyrl','Deva','Guru','Hans','Hrkt','Latn','Mong','Mymr','Orya','Zxxx','Zzzz'], }, }, // DF durationformat: { 'digital': {'a': {'milliseconds': 1}}, 'long': {'a': {'years': 1, 'microseconds': 1}, 'b': {'seconds': 2}}, 'narrow': {'a': {'years': 1, 'months': 2, 'seconds': 1, 'microseconds': 1000}}, 'short': {'a': {'days': 2, 'seconds': 2, 'nanoseconds': 1}}, }, // NF 'numberformat.compact': {'long': [0/0, 1000, 2e6, 6.6e12, 7e15],'short': [-1100000000, -1000],}, 'numberformat.currency': {"KES": curS, 'ETB': curN, "GBP": curS, "USD": curAN, "XXX": curN}, 'numberformat.formattoparts': { 'decimal': [1.2],'group': [1000, 99999],'infinity': [Infinity],'minusSign': [-5],'nan': ['a'] }, 'numberformat.notation': { scientific: {'decimal': []}, standard: {'decimal': [0/0, -1000, 987654], 'percent': [1000]}, }, 'numberformat.sign': {always: [-1, 0/0]}, 'numberformat.unit': { 'byte': unitN, // ICU 74 'fahrenheit': unitB, 'foot': unitL, 'hectare': {'long': [1], 'short': [987654]}, 'kilometer-per-hour': unitN, 'millimeter': unitN, 'month': unitB, 'nanosecond': unitN, 'percent': {"long": [1], "narrow": [1], "short": [987654]}, 'second': {'long': [1], 'narrow': [1], 'short': [987654]}, 'terabyte': unitL, }, // PR 'pluralrules.select': { cardinal: [0, 1, 2, 3, 7, 21, 100], ordinal: [1, 2, 3, 4, 5, 6, 8, 10, 81] }, 'pluralrules.selectrange': { cardinal: [[0,0],[1,1],[2,1],[2,4]], ordinal: [[0,0],[0,1],[0,6],[1,1],[1,3],[1,5],[3,3]], }, // other relativetimeformat: { always: {'narrow': [[1, 'day'], [0, 'year']]}, auto: { 'long': [[1, 'second']], 'narrow': [[0,'second'],[1,'second'],[3,'second'],[0,'day'],[1,'day'], [3,'day'],[1,'week'],[0,'quarter'],[1,'year']] }, }, resolvedoptions: { collator: ['caseFirst'], datetimeformat: ['calendar','day','hourcycle','month','numberingSystem'], pluralrules: ['pluralCategories'], }, } try {oIntlLocale['numberformat.compact']['long'].push(BigInt('987354000000000000'))} catch {} let nBig = 987654 try {nBig = BigInt('987354000000000000')} catch {} oIntlLocale['numberformat.notation']['scientific']['decimal'].push(nBig) // build keys for (const k of Object.keys(oIntlLocale)) { oIntlLocaleKeys[k] = [] for (const j of Object.keys(oIntlLocale[k]).sort()) {oIntlLocaleKeys[k].push(j)} } } function get_geo(METRIC) { // nav/window are redundant: display only let res = [], value, notation = default_red, isLies = false // nav try { let keys = Object.keys(Object.getOwnPropertyDescriptors(Navigator.prototype)) if (runSL) { keys = keys.filter(x => !['geolocation'].includes(x)) keys.push('geolocation') } value = keys.includes(METRIC) ? zE : zD // this only detects enabled as untrustworthy if (keys.indexOf(METRIC) > keys.indexOf('constructor')) { log_known(4, METRIC +'_navigator', value) isLies = true } } catch(e) { log_error(4, METRIC +'_navigator', e); value = zErr } addDisplay(4, METRIC +'_navigator', value,'','', isLies) // display separate for notating lies res.push(isLies? zLIE : value) // window try { value = 'Geolocation' in window ? true : false if (value == true) { let typeCheck = typeFn(window.Geolocation) if (runST) {typeCheck = 'string'} if ('function' !== typeCheck) {throw zErrType + typeCheck} } } catch(e) { log_error(4, METRIC +'_window', e); value = zErr } res.push(value) // summary let hash = mini(res) if (isBB && hash == 'feacff5d') { notation = default_green // BB ESR78+: disabled, true } else if (!isBB && hash == '23d43ed0') { notation = default_green // FF72+: enabled, true } // health lookup if (gRun) {sDetail[isScope].lookup[METRIC] = res.join(' | ')} addDisplay(4, METRIC, res[1],'', notation) return } function get_language_locale() { // reset isLocaleValid = false isLocaleValue = undefined isLocaleAlt = undefined isLanguagesNav = [] // LANGUAGES function get_langmetric(m) { try { let value = navigator[m] let expected = ('language' == m ? 'string' : 'array') if (runST) {value = ('language' == m ? null : [])} let typeCheck = typeFn(value) if (expected !== typeCheck) {throw zErrType + typeCheck} if ('languages' == m) { value.forEach(function(l){ isLanguagesNav.push(l.toLowerCase()) }) } return ('language' == m ? value : value.join(', ')) } catch(e) { return [e] } } let oData = {}, metrics = ['language','languages'], notation ='' metrics.forEach(function(m) {oData[m] = get_langmetric(m)}) Object.keys(oData).forEach(function(METRIC){ if (isLanguageSmart && isBB) { // only notate BB notation = bb_red if (languagesSupported[oData.language] !== undefined) { if ('language' == METRIC) {notation = bb_green } else {if (oData[METRIC] == oData.language +', '+ languagesSupported[oData.language][0]) {notation = bb_green} } } } let value = oData[METRIC], data ='' if ('array' == typeFn(value)) {value = value[0]; data = zErrLog} addBoth(4, METRIC, value,'', notation, data, isProxyLie('Navigator.'+ METRIC)) }) // LOCALES function get_locmetric(m) { let METRIC = 'locale_'+ m, r try { if ('collator' == m) {if (runSL) {r = 'en-FAKE'} else {r = Intl.Collator().resolvedOptions().locale} } else if ('datetimeformat' == m) {r = Intl.DateTimeFormat().resolvedOptions().locale } else if ('displaynames' == m) {r = new Intl.DisplayNames(undefined, {type: 'region'}).resolvedOptions().locale } else if ('durationformat' == m) {r = new Intl.DurationFormat().resolvedOptions().locale } else if ('listformat' == m) {r = new Intl.ListFormat().resolvedOptions().locale } else if ('numberformat' == m) {r = new Intl.NumberFormat().resolvedOptions().locale } else if ('pluralrules' == m) {r = new Intl.PluralRules().resolvedOptions().locale } else if ('relativetimeformat' == m) {r = new Intl.RelativeTimeFormat().resolvedOptions().locale } else if ('segmenter' == m) {r = new Intl.Segmenter().resolvedOptions().locale } if (runST) {r = undefined} else if (runSI) {r = 'collator' !== m ? 'en-USA' : 'tzp'} let typeCheck = typeFn(r) if ('string' !== typeCheck) {throw zErrType + typeCheck} if (!Intl.DateTimeFormat.supportedLocalesOf([r]).length) {throw zErrInvalid + 'locale '+ r +' not supported'} oRes[m] = r return r } catch(e) { oRes[m] = e+'' log_error(4, METRIC, e) oErr[m] = e+'' return zErr } } // LOCALES let METRIC = 'locale', value ='', res = [], oRes = {}, oErr = {} metrics = [ 'collator','datetimeformat','displaynames','durationformat','listformat', 'numberformat','pluralrules','relativetimeformat','segmenter', ] metrics.forEach(function(m) {res.push(get_locmetric(m))}) sDetail[isScope][METRIC] = oRes let btn = addButton(4, METRIC) // LOCALE // remove errors + dupes res = res.filter(x => ![zErr].includes(x)) res = dedupeArray(res) let isLies = false if (res.length == 1) { value = res[0] isLocaleValue = value // reduce en health false positives // but only for isBB since as it only ships with en-US // use isLocaleAlt in validation checks: allow e.g. en-CA to use en-US for lookup // ^ we already have a health check for wrong locale isLocaleAlt = (isBB && 'en-' == isLocaleValue.slice(0,3) ? 'en-US' : isLocaleValue) if (isSmart) {isLocaleValid = true} // only set if smart } else if (res.length == 0) { value = zErr } else { value = 'mixed'; isLies = true } if (isLanguageSmart && isBB) { // only notate BB notation = bb_red let errHash = mini(oErr) if (Object.keys(oErr).length == 0) { // BB15: no errors // only green if BB supported let key = oData.language if (languagesSupported[key] !== undefined) { let expected = languagesSupported[key][1] == undefined ? key : languagesSupported[key][1] if (value === expected) {notation = bb_green} } } } addDisplay(4, METRIC, value, btn, notation, isLies) addData(4, METRIC, value, '', isLies) return } function get_language_system(METRIC) { /* systemLanguages: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/systemLanguage populate svg with nav entries to detect if anything added. To detect removals would mean populating with all supported BCPs (lots) = not worth it: perf and it is unlikely _only_ removal happens, i.e we already detect added. Also prior to FF127 = false positives with prefixs e.g. if you were 'en-US, en', all en-* would be be true. Not worth the footgun or hasssle */ let t0 = nowFn() let value, data ='' try { isLanguagesNav.sort() // so results are sorted // populate let aText = ['<switch id="switch">'] isLanguagesNav.forEach(function(l){aText.push('<text systemLanguage="'+ l +'">' + l +'</text>')}) aText.push('<text systemLanguage="groot">groot</text>') aText.push('<text>unknown</text></switch>') let el = dom.tzpSwitch el.innerHTML = aText.join('') // walk nodes let aDetected = [] const walker = document.createTreeWalker(dom['switch'], NodeFilter.SHOW_TEXT, null); while(walker.nextNode() && walker.currentNode) { let target = walker.currentNode //* important: we check range.getClientRects DOMRectList length so only real nav items are detected // we use range due to selectNode (I think) // we can't use range.getBoundingClientRect's DOMRect object (can't get obj keys length) // THIS IS THE WAY: range.getClientRects() // e.g. if isLanguagesNav has a fake 'fr' (e.g. extension) it won't be detected as it // isn't a "rendered" node with a range (cuz it's fake) - IIUIC let range = new Range() range.selectNode(target) if (range.getClientRects().length) {aDetected.push(target.textContent)} } // remove unknown aDetected = aDetected.filter(x => !['unknown'].includes(x)) if (0 == aDetected.length) {throw zErrType + 'empty array'} value = aDetected.join(', ') } catch(e) { value = e; data = zErrLog } // tidy nav string to compare to isLanguagesNav = isLanguagesNav.join(', ') addBoth(4, METRIC, value,'', (value == isLanguagesNav ? lang_green : lang_red), data) log_perf(4, METRIC, t0) } function get_dates_intl() { function get_metric(m, isIntl) { let tsub0 = nowFn(), countC = 0 try { let obj = {}, objcheck = {}, tests = oIntlDate[m], testkeys = oIntlDateKeys[m], value let formatter, checker if ('date_timestyle' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] obj[key] = {}; objcheck[key] = {} Object.keys(tests[key]).forEach(function(s) { let data = [], datacheck = [] let styles = s.split('_'), cal = 'default' == key ? undefined : key if (1 == styles.length) {styles.push(styles[0])} // ensure we have two styles // test let options = {dateStyle: styles[0], timeStyle: styles[1], timeZone: tzTest} if ('default' !== key) {options['calendar'] = key} formatter = Intl.DateTimeFormat(locTest, options); countC++ // check if (isCheck) { options = {calendar: cal, dateStyle: styles[0], timeStyle: styles[1], timeZone: tzCheck} checker = Intl.DateTimeFormat(locCheck, options); countC++ } tests[key][s].forEach(function(n) { value = formatter.format(n); data.push(value) if (isCheck) {value = checker.format(n); datacheck.push(value)} }) obj[key][s] = data; objcheck[key][s] = datacheck }) } } // microperf add_microperf_intl('datetimeformat.'+ m, countC, tsub0, isIntl) // return return [ {'hash': mini(obj), 'metrics': obj}, (isCheck ? {'hash': mini(objcheck), 'metrics': objcheck} : undefined) ] } catch(e) { add_microperf_intl('datetimeformat.'+ m, countC, tsub0, isIntl) log_error(4, METRIC +'_'+ m, e) return [zErr, zErr] } } const oMetrics = { intl : ['date_timestyle',], 'to-string': [], } let METRIC, oStringExpected = {}, isCheck = isLocaleValid && isTimeZoneValid let locTest = undefined, locCheck = isLocaleValue // use variables so I can test them let tzTest = undefined, tzCheck = isTimeZoneValue // use variables so I can test them Object.keys(oMetrics).forEach(function(list){ METRIC = 'dates_'+ list let t0 = nowFn(), isIntl = 'intl' == list, notation = localetz_red let oData = {}, oCheck = {} // data from each intl/string loop oMetrics[list].forEach(function(m) { let res = get_metric(m, isIntl) oData[m] = res[0] oCheck[m] = res[1] let isString = (isIntl && oMetrics['to-string'].includes(m)) if (isString) {oStringExpected[m] = res[0]} // intl version of to*string to compare to // console.log(list, res); console.log('test', oData); console.log('check', oCheck); console.log('expected string', oStringExpected) }) let hash = mini(oData) // on string loop (we have an empty tostring list hence the extra check) if (!isIntl && oMetrics[list].length) { // does the undefined string data match the undefined intl data addDisplay(4, METRIC +'_matches_intl','','', (hash == mini(oStringExpected) ? intl_green : intl_red)) } if (isCheck) { if (hash == mini(oCheck)) { notation = localetz_green } else { addDetail(METRIC +'_expected', oCheck) notation = addButton('bad', METRIC +'_expected', "<span class='health'>"+ cross +"</span> locale + timezone") } } if (oMetrics[list].length) { // temp check until we start building string tests addBoth(4, METRIC, hash, addButton(4, METRIC), notation, oData) log_perf(4, METRIC, t0) } if (!isIntl) {return} }) } function get_locale_intl() { function get_metric(m, isIntl) { let tsub0 = nowFn(), countC = 0 try { let obj = {}, objcheck = {}, tests = oIntlLocale[m], testkeys = oIntlLocaleKeys[m], value let formatter, checker if ('collation' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let testdata = tests[key].sort() // always resort // trim leading/trailing spacesto help LTR/RTL obj[key] = testdata.sort(Intl.Collator(locTest, {usage: key}).compare).join(' , ').trim(); countC++ if (isCheck) {objcheck[key] = testdata.sort(Intl.Collator(locCheck, {usage: key}).compare).join(' , ').trim(); countC++} } } else if ('datetimeformat.components' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] obj[key] = {}; objcheck[key] = {} Object.keys(tests[key]).forEach(function(s) { let option = tests[key][s][0] // our dates are specifically UTC to get specific days/hrs // to preserve that we pass UTC as the timeZone option['timeZone'] = 'UTC' let data = [], datacheck = [] if (isIntl) { formatter = new Intl.DateTimeFormat(locTest, option); countC++ if (isCheck) {checker = new Intl.DateTimeFormat(locCheck, option); countC++} } tests[key][s][1].forEach(function(n){ value = (isIntl ? formatter.format(n) : (n).toLocaleString(strTest, option)); data.push(value) if (isCheck) {value = (isIntl ? checker.format(n) : (n).toLocaleString(strCheck, option)); datacheck.push(value)} }) obj[key][s] = data; objcheck[key][s] = datacheck }) } } else if ('datetimeformat.dayperiod' == m) { // our dates are specifically UTC to get specific times // to preserve that we pass UTC as the timeZone for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = [], datacheck = [] formatter = new Intl.DateTimeFormat(locTest, {hourCycle: 'h12', timeZone: 'UTC', dayPeriod: key}); countC++ if (isCheck) {checker = new Intl.DateTimeFormat(locCheck, {hourCycle: 'h12', timeZone: 'UTC', dayPeriod: key}); countC++} tests[key].forEach(function(item) { data.push(formatter.format(item)) if (isCheck) {datacheck.push(checker.format(item))} }) obj[key] = data; objcheck[key] = datacheck } } else if ('datetimeformat.listformat' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = [], datacheck = [] tests[key].forEach(function(item) { data.push(new Intl.ListFormat(locTest, {style: key, type: item}).format(['a','b','c'])); countC++ if (isCheck) {datacheck.push(new Intl.ListFormat(locCheck, {style: key, type: item}).format(['a','b','c'])); countC++} }) obj[key] = data; objcheck[key] = datacheck } } else if ('datetimeformat.relatedyear' == m) { // our dates are specifically UTC as we expose the day and // to preserve that we pass UTC as the timeZone for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let cal = 'default' == key ? undefined : key let data = [], datacheck = [] if (isIntl) { formatter = Intl.DateTimeFormat(locTest, {calendar: cal, relatedYear: 'long', timeZone: 'UTC'}); countC++ if (isCheck) {checker = Intl.DateTimeFormat(locCheck, {calendar: cal, relatedYear: 'long', timeZone: 'UTC'}); countC++} } tests[key].forEach(function(d) { let stroptions = {calendar: cal, day: 'numeric', month: 'numeric', year: 'numeric', timeZone: 'UTC'} value = (isIntl ? formatter.format(d) : (d).toLocaleString(strTest, stroptions)); data.push(value) if (isCheck) {value = (isIntl ? checker.format(d) : (d).toLocaleString(strCheck, stroptions)); datacheck.push(value)} }) obj[key] = data; objcheck[key] = datacheck } } else if ('datetimeformat.timezonename' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = [], datacheck = [] Object.keys(tests[key]).forEach(function(tzn){ try { // use y+m+d numeric so toLocaleString matches // use hour12 in case - https://bugzilla.mozilla.org/show_bug.cgi?id=1645115#c9 // key: e.g. Africa/Douala | tzn: e.g. longGeneric let option = {year: 'numeric', month: 'numeric', day: 'numeric', hour12: true, timeZone: key, timeZoneName: tzn} if (isIntl) { formatter = Intl.DateTimeFormat(locTest, option); countC++ if (isCheck) {checker = Intl.DateTimeFormat(locCheck, option); countC++} } tests[key][tzn].forEach(function(dte){ value = (isIntl ? formatter.format(dte) : (dte).toLocaleString(strTest, option)); data.push(value) if(isCheck) {value = (isIntl ? checker.format(dte) : (dte).toLocaleString(strCheck, option)); datacheck.push(value)} }) } catch {} // ignore invalid if (data.length) {obj[key] = data} if (datacheck.length) {objcheck[key] = datacheck} }) } if (!Object.keys(obj).length) {let trap = Intl.DateTimeFormat(locTest, {timeZoneName: 'longGeneric'})} // trap error } else if ('displaynames' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] obj[key] = {}; objcheck[key] = {} Object.keys(tests[key]).forEach(function(s) { // for each style let optkey = 'datetimefield' == key ? 'dateTimeField' : key // fix key case let options = {type: optkey, style: s} if ('language' == key) {options = {type: key, languageDisplay: s}} let data = {}, datacheck = {} // displaynames takes an empty array for undefined, but allow oour override for testing let locIntl = undefined == locTest ? [] : locTest formatter = new Intl.DisplayNames(locIntl, options); countC++ if (isCheck) {checker = new Intl.DisplayNames(locCheck, options); countC++} tests[key][s].forEach(function(item) { data[item] = formatter.of(item) if (isCheck) {datacheck[item] = checker.of(item)} }) obj[key][s] = data; objcheck[key][s] = datacheck }) } } else if ('durationformat' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let yearformat = ('long' == key || 'short' == key) ? 'always' : 'auto' // long we want to force 0 for years formatter = new Intl.DurationFormat(locTest, {style: key, yearsDisplay: yearformat}); countC++ if (isCheck) {checker = new Intl.DurationFormat(locCheck, {style: key, yearsDisplay: yearformat}); countC++} let data = [], datacheck = [] for (const item of Object.keys(tests[key])) { data.push(formatter.format(tests[key][item])) if (isCheck) {datacheck.push(checker.format(tests[key][item]))} } obj[key] = data.join(' | '); objcheck[key] = datacheck.join(' | ') } } else if ('numberformat.compact' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let option = {notation: 'compact', compactDisplay: key, useGrouping: true} let data = [], datacheck = [] if (isIntl) { formatter = new Intl.NumberFormat(locTest, option); countC++ if (isCheck) {checker = new Intl.NumberFormat(locCheck, option); countC++} } tests[key].forEach(function(n) { value = (isIntl ? formatter.format(n) : (n).toLocaleString(strTest, option)); data.push(value) if (isCheck) {value = (isIntl ? checker.format(n) : (n).toLocaleString(strCheck, option)); datacheck.push(value)} }) obj[key] = data; objcheck[key] = datacheck } } else if ('numberformat.currency' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] obj[key] = {}; objcheck[key] = {} Object.keys(tests[key]).forEach(function(s) { let option = 'accounting' == s ? {style: 'currency', currency: key, currencySign: s} : {style: 'currency', currency: key, currencyDisplay: s} let data = [], datacheck = [] tests[key][s].forEach(function(n) { value = (isIntl ? Intl.NumberFormat(locTest, option).format(n) : (n).toLocaleString(strTest, option)) data.push(value); countC++ if (isCheck) { value = (isIntl ? Intl.NumberFormat(locCheck, option).format(n) : (n).toLocaleString(strCheck, option)) datacheck.push(value); countC++ } }) obj[key][s] = data; objcheck[key][s] = datacheck }) } } else if ('numberformat.formattoparts' == m) { function get_value(type, aParts) { for (let i=0; i < aParts.length; i++) { if (aParts[i].type === type) {str = aParts[i].value; return (str.length == 1 ? str.charCodeAt(0) : str)} } return 'none' } formatter = Intl.NumberFormat(locTest); countC++ if (isCheck) {checker = Intl.NumberFormat(locCheck); countC++} let str for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = [], datacheck = [] tests[key].forEach(function(num){ data.push(get_value(key, formatter.formatToParts(num))) if (isCheck) {datacheck.push(get_value(key, checker.formatToParts(num)))} }) obj[key] = data; objcheck[key] = datacheck } } else if ('numberformat.notation' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] obj[key] = {}; objcheck[key] = {} Object.keys(tests[key]).forEach(function(s) { let data = [], datacheck = [] if (isIntl) { formatter = Intl.NumberFormat(locTest, {notation: key, style: s}); countC++ if (isCheck) {checker = Intl.NumberFormat(locCheck, {notation: key, style: s}); countC++} } tests[key][s].forEach(function(n){ value = (isIntl ? formatter.format(n) : (n).toLocaleString(strTest, {notation: key, style: s})); data.push(value) if (isCheck) {value = (isIntl ? checker.format(n) : (n).toLocaleString(strCheck, {notation: key, style: s})); datacheck.push(value)} }) obj[key][s] = data; objcheck[key][s] = datacheck }) } } else if ('numberformat.sign' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = [], datacheck = [] if (isIntl) { formatter = new Intl.NumberFormat(locTest, {signDisplay: key}); countC++ if (isCheck) {checker = new Intl.NumberFormat(locCheck, {signDisplay: key}); countC++} } tests[key].forEach(function(n){ value = (isIntl ? formatter.format(n) : (n).toLocaleString(strTest, {signDisplay: key})); data.push(value) if (isCheck) {value = (isIntl ? checker.format(n) : (n).toLocaleString(strCheck, {signDisplay: key})); datacheck.push(value)} }) obj[key] = data; objcheck[key] = datacheck } } else if ('numberformat.unit' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = [], datacheck = [] Object.keys(tests[key]).forEach(function(ud){ try { if (isIntl) { formatter = Intl.NumberFormat(locTest, {style: 'unit', unit: key, unitDisplay: ud}); countC++ if (isCheck) {checker = Intl.NumberFormat(locCheck, {style: 'unit', unit: key, unitDisplay: ud}); countC++} } tests[key][ud].forEach(function(n){ value = (isIntl ? formatter.format(n) : (n).toLocaleString(strTest, {style: 'unit', unit: key, unitDisplay: ud})) data.push(value) if (isCheck) { value = (isIntl ? checker.format(n) : (n).toLocaleString(strCheck, {style: 'unit', unit: key, unitDisplay: ud})) datacheck.push(value) } }) } catch {} // ignore invalid }) if (data.length) {obj[key] = data} if (datacheck.length) {objcheck[key] = datacheck} } if (!Object.keys(obj).length) {let trap = Intl.NumberFormat(locTest, {style: 'unit', unit: 'day'})} // trap error } else if ('pluralrules.select' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = [], datacheck = [] formatter = new Intl.PluralRules(locTest, {type: key}); countC++ if (isCheck) {checker = new Intl.PluralRules(locCheck, {type: key}); countC++} let prev='', current='', prevchk='', currentchk='' tests[key].forEach(function(n) { current = formatter.select(n); if (prev !== current) {data.push(n +': '+ current); prev = current} if (isCheck) { currentchk = checker.select(n); if (prevchk !== currentchk) {datacheck.push(n +': '+ currentchk); prevchk = currentchk} } }) obj[key] = data; objcheck[key] = datacheck } } else if ('pluralrules.selectrange' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] let data = {}, datacheck = {} formatter = new Intl.PluralRules(locTest, {type: key}); countC++ if (isCheck) {checker = new Intl.PluralRules(locCheck, {type: key}); countC++} let prev='', current='', prevchk='', currentchk='' tests[key].forEach(function(n) { current = formatter.selectRange(n[0], n[1]) if (prev !== current) { let datakey = formatter.select(n[0]) +'-'+ formatter.select(n[1]) if (undefined == data[datakey]) {data[datakey] = current} prev = current } if (isCheck) { currentchk = checker.selectRange(n[0], n[1]) if (prevchk !== currentchk) { let checkkey = checker.select(n[0]) +'-'+ checker.select(n[1]) if (undefined == datacheck[checkkey]) {datacheck[checkkey] = currentchk} prevchk = currentchk } } }) // sort obj keys let newdata = {}, newdatacheck = {} for (const k of Object.keys(data).sort()) {newdata[k] = data[k]} for (const k of Object.keys(datacheck).sort()) {newdatacheck[k] = datacheck[k]} obj[key] = newdata; objcheck[key] = newdatacheck } } else if ('relativetimeformat' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] obj[key] = {}; objcheck[key] = {} Object.keys(tests[key]).forEach(function(s) { let data = [], datacheck = [] formatter = new Intl.RelativeTimeFormat(locTest, {style: s, numeric: key}); countC++ if (isCheck) {checker = new Intl.RelativeTimeFormat(locCheck, {style: s, numeric: key}); countC++} tests[key][s].forEach(function(pair){ data.push(formatter.format(pair[0], pair[1])) if (isCheck) {datacheck.push(checker.format(pair[0], pair[1]))} }) obj[key][s] = data; objcheck[key][s] = datacheck }) } } else if ('resolvedoptions' == m) { for (let i=0; i < testkeys.length; i++) { let key = testkeys[i] if ('collator' == key) {formatter = Intl.Collator(locTest).resolvedOptions(); countC++ } else if ('datetimeformat' == key) {formatter = Intl.DateTimeFormat(locTest).resolvedOptions(); countC++ } else if ('pluralrules' == key) {formatter = new Intl.PluralRules(locTest).resolvedOptions(); countC++ } if (isCheck) { if ('collator' == key) {checker = Intl.Collator(locCheck).resolvedOptions(); countC++ } else if ('datetimeformat' == key) {checker = Intl.DateTimeFormat(locCheck).resolvedOptions(); countC++ } else if ('pluralrules' == key) {checker = new Intl.PluralRules(locCheck).resolvedOptions(); countC++ } } obj[key] = {}; objcheck[key] = {} tests[key].forEach(function(s){ if ('hourcycle' == s) {value = Intl.DateTimeFormat(locTest, {hour: 'numeric'}).resolvedOptions().hourCycle; countC++ } else if ('pluralCategories' == s) {value = formatter[s].join(', ') } else {value = formatter[s]} obj[key][s] = value if (isCheck) { if ('hourcycle' == s) {value = Intl.DateTimeFormat(locCheck, {hour: 'numeric'}).resolvedOptions().hourCycle; countC++ } else if ('pluralCategories' == s) {value = checker[s].join(', ') } else {value = checker[s]} objcheck[key][s] = value } }) } } // microperf add_microperf_intl(m, countC, tsub0, isIntl) // return return [ //{'hash': mini(obj), 'metrics': obj}, //(isCheck ? {'hash': mini(objcheck), 'metrics': objcheck} : undefined) obj, (isCheck ? objcheck : undefined) ] } catch(e) { add_microperf_intl(m, countC, tsub0, isIntl) log_error(4, METRIC +'_'+ m.replace('.','_'), e) return [zErr, zErr] } } const oMetrics = { intl : [ 'collation', 'datetimeformat.components','datetimeformat.dayperiod','datetimeformat.listformat', 'datetimeformat.relatedyear','datetimeformat.timezonename', 'displaynames','durationformat', 'numberformat.compact','numberformat.currency','numberformat.formattoparts', 'numberformat.notation','numberformat.sign','numberformat.unit', 'pluralrules.select','pluralrules.selectrange','relativetimeformat','resolvedoptions', ], tolocalestring: [ 'datetimeformat.components','datetimeformat.relatedyear','datetimeformat.timezonename', 'numberformat.compact','numberformat.currency','numberformat.notation','numberformat.sign','numberformat.unit', ], } let METRIC, isCheck = isLocaleValid let oStringExpected = {}, oStringExpectedChildren = {} let locTest = undefined, locCheck = isLocaleValue // use variables so I can test them let strTest = undefined, strCheck = isLocaleValue //locTest = 'de'; locCheck = 'de' // should be the same //locTest = 'it'; locCheck = 'ko' // everything should be different //strTest = 'fr', strCheck = 'fr' // should be the same //strTest = 'pl', strCheck = 'es' // everything should be different Object.keys(oMetrics).forEach(function(list){ METRIC = 'locale_'+ list let t0 = nowFn(), isIntl = 'intl' == list, notation = locale_red let oData = {}, oCheck = {} // data from each intl/string loop let oDataChildren = {}, oCheckChildren = {} oMetrics[list].forEach(function(m) { let res = get_metric(m, isIntl) let isParent = m.includes('.') let isString = (isIntl && oMetrics['tolocalestring'].includes(m)) if (isParent) { let parent = m.split('.')[0], child = m.split('.')[1] // placeholders (so sorted order is kelp) oData[parent] = {} oCheck[parent] = {} // children if (undefined == oDataChildren[parent]) {oDataChildren[parent] = {}} oDataChildren[parent][child] = res[0] if (undefined == oCheckChildren[parent]) {oCheckChildren[parent] = {}} oCheckChildren[parent][child] = res[1] if (isString) { oStringExpected[parent] = {} if (undefined == oStringExpectedChildren[parent]) {oStringExpectedChildren[parent] = {}} oStringExpectedChildren[parent][child] = res[0] } } else { // direct: don't hash zErr oData[m] = zErr == res[0] ? zErr : {'hash': mini(res[0]), 'metrics': res[0]} oCheck[m] = zErr == res[1] ? zErr: {'hash': mini(res[1]), 'metrics': res[1]} if (isString) { oStringExpected[m] = zErr == res[0] ? zErr : {'hash': mini(res[0]), 'metrics': res[0]} } } }) // update placeholders for (const k of Object.keys(oDataChildren)) { oData[k] = {'hash': mini(oDataChildren[k]), 'metrics': oDataChildren[k]} oCheck[k] = {'hash': mini(oCheckChildren[k]), 'metrics': oCheckChildren[k]} } let hash = mini(oData) // update expected string placeholder on the intl loop if (isIntl) { for (const k of Object.keys(oStringExpectedChildren)) { oStringExpected[k] = {'hash': mini(oStringExpectedChildren[k]), 'metrics': oStringExpectedChildren[k]} } } else { // on string loop compare it // does the undefined string data match the undefined intl data addDisplay(4, METRIC +'_matches_intl','','', (hash == mini(oStringExpected) ? intl_green : intl_red)) } if (isCheck) { if (hash == mini(oCheck)) { notation = locale_green } else { addDetail(METRIC +'_expected', oCheck) notation = addButton('bad', METRIC +'_expected', "<span class='health'>"+ cross +"</span> locale") } } addBoth(4, METRIC, hash, addButton(4, METRIC), notation, oData) log_perf(4, METRIC, t0) if (!isIntl) {return} }) } function get_timezone(METRIC) { // reset isTimeZoneValid = false isTimeZoneValue = undefined let tzo = get_timezone_offset(METRIC +'_offset') let offsets = get_timezone_offsets(METRIC +'_offsets', tzo.nowValue, tzo.utcValue) // timezone: we can use tzo.tampered items to return if isLies let aMethods = ['timeZone','timeZoneId','zonedDateTimeISO'] let aTemporal = ['plainDateISO','plainDateTimeISO','plainTimeISO','zonedDateTimeISO'] let errCount = 0, lieCount = 0, tzData = {'data': [], 'valid': []}, notation = rfp_red, isLies = false aMethods.forEach(function(k) { let tz isLies = false try { if ('timeZone' == k) { tz = Intl.DateTimeFormat().resolvedOptions().timeZone if (tzo.tampered.includes('timeZone')) {isLies = true} //tz = 'Asia/Tokyo' // test mixed but no lies detected } else { if (tzo.tampered.filter(x => aTemporal.includes(x)).length) {isLies = true} if ('timeZoneId' == k) { tz = Temporal.Now.timeZoneId() } else { tz = Temporal.Now.zonedDateTimeISO().toString() tz = tz.slice(tz.indexOf('[') + 1, tz.length - 1) } } if (runST) {tz = undefined} else if (runSI) {tz = 'tzp'} let typeCheck = typeFn(tz) if ('string' !== typeCheck) {throw zErrType + typeCheck} let tztest = (new Date('January 1, 2018 13:00:00 UTC')).toLocaleString('en', {timeZone: tz}) tzData.data.push(tz) if (isLies) {lieCount++; log_known(4, METRIC +'_'+ k, tz)} else {tzData.valid.push(tz)} addDisplay(4, METRIC +'_'+ k, tz, '','', isLies) } catch(e) { errCount++ addDisplay(4, METRIC +'_'+ k, log_error(4, METRIC +'_'+ k, e)) } }) // all errors if (errCount == aMethods.length) {addBoth(4, METRIC, zErr +'s', '', notation, zErr); return} // notation: 3 x truthful Atlantic/Reykjavik, and whilst we already have health checks // on offsets(s) but we need to confirm the actual results if ('80724dcd' == mini(tzData) && 0 === tzo.nowValue && '031b56a9' == offsets.hash) {notation = rfp_green} // summary // if we have a single valid value, use that // if valid is mixed then data is also mixed // if valid is empty then we have to use data anyway // data will always have at least one value (we returned earlier if all errors) let value = '', aValid = dedupeArray(tzData.valid), aData = dedupeArray(tzData.data) let isMixed = aData.length > 1, isValid = 1 == aValid.length isLies = !isValid if (1 == aValid.length) {value = aValid[0]} else {if (isMixed) {value = 'mixed'} else {value = aData[0]}} // isTimeZoneValue if ('mixed' !== value) {isTimeZoneValue = value} // health lookup if (notation == rfp_red && 'Atlantic/Reykjavik' == value) { // then we must have had lies and/or errors let aHealth = [] if (errCount > 0) {aHealth.push(errCount + ' error' + (errCount == 1 ? '' : 's'))} if (lieCount > 0) {aHealth.push(lieCount + ' mismatch' + (lieCount == 1 ? '' : 'es'))} if (gRun) {sDetail[isScope].lookup[METRIC] = aHealth.join(' | ')} } // display addBoth(4, METRIC, value, '', notation, '', isLies) // set isTimeZoneValid // offset: no tampering ignore errors | offsets : no tampering or errors (I might need to revisit this logic later) // ^ this means anything date/temporal or to*string hasn't been tampered with // timezone: can't be any lies and can't be mixed let isTZValidSoFar = 0 == tzo.tampered.length && true === offsets.health if (isTZValidSoFar) { if (0 == lieCount && 1 == aValid.length) {isTimeZoneValid = true} } return } function get_timezone_offset(METRIC) { // this is good test to catch + record various temporal/date/toString lies even // if they are ultimately duplicitous. This requires the spoofed offset to differ // from lastModified and real-time real-world world offsets only number 65-70 // IIUIC. So not definitive, but multiple exposure of tampering is good. also, // fuck extensions trying to resist or solutions that create mismatches :) let t0 = nowFn() // setup const xslText = '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"' +' xmlns:date="http://exslt.org/dates-and-times" extension-element-prefixes="date"><xsl:output method="html"/>' +' <xsl:template match="/"><xsl:value-of select="date:date-time()" /></xsl:template></xsl:stylesheet>' const doc = (new DOMParser).parseFromString(xslText, 'text/xml') let oData = {}, notation = tz_red let methods = [ 'timeZone', // intl.DTF 'iframe','parseFromString','parseHTMLUnsafe', // last modified, also exslt 'plainDateISO','plainDateTimeISO','zonedDateTimeISO','plainTimeISO', // temporal // to*string 'toDateString','toLocaleString','toLocaleDateString', 'toLocaleTimeString','toString','toTimeString', //'date', ] // non-gecko: skip exslt // 1990759: ToDo: add isXSLT + isVer when the pref flips: dom.xslt.enabled if (isGecko) {methods.push('exslt')} else {addDisplay(4, METRIC +'_exslt', zNA)} methods.sort() let aLastMods = ['exslt','iframe','parseFromString','parseHTMLUnsafe'] // is lastModified source let testdate let tznShort = { AKST: '-09:00', AKDT: '-08:00', AST: '-04:00', ADT: '-03:00', CST: '-06:00', CDT: '-05:00', EST: '-05:00', EDT: '-04:00', HAST: '-10:00', HADT: '-09:00', HST: '-10:00', HDT: '-09:00', MST: '-07:00', MDT: '-06:00', PST: '-08:00', PDT: '-07:00', UTC: '+00:00', GMT: '+00:00', GMT0: '+00:00', } function create_offset() { // if we don't have a minutekey but we do have a non-exslt lastmod, we can // check timezones and compare/calulate to determine a minutekey/offset let key = oData.hasLastMod[0] if (undefined == key) {return} let aTZs = [ // these are timezones, not short timezonenames 'GMT','GMT+1','GMT+2','GMT+3','GMT+4','GMT+5','GMT+6','GMT+7','GMT+8','GMT+9','GMT+10','GMT+11','GMT+12', 'GMT-1','GMT-2','GMT-3','GMT-4','GMT-5','GMT-6','GMT-7','GMT-8','GMT-9','GMT-10','GMT-11','GMT-12','GMT-13','GMT-14', ] let option = { day: '2-digit', month: '2-digit', year: 'numeric', hour12: false, hour: '2-digit', minute: 'numeric', second: 'numeric', timeZoneName: 'short' } // note: testdate was already set in get_values so it's identical here // note: aTZs covers the hardcoded values in tznShort // note: this only covers full hours if an exact match: we will use 10's of minutes accuracy // to reduce chances of a digit having ticked over // lastMods iframe/parse* raw format's datetime components matches formatter let exactmatch = oData.raw[key].slice(0,15), hourmatch = oData.raw[key].slice(0,13) let isPartial = false, offset, value for (let i = 0; i < aTZs.length; i++) { try { option.timeZone = 'Etc/'+ aTZs[i] let formatter = new Intl.DateTimeFormat('en', option) value = formatter.format(testdate).replace(',','') offset = value.split(' ')[2] if (value.slice(0,13) == hourmatch) { if (value.slice(0,15) == exactmatch) { //console.log(value.slice(0,15), 'exact match', offset) // exact match oData.minutekey = key oData.offset[key] = offset break } else { //console.log(value.slice(0,15), 'hour match', offset) // hour match // this works because we use the extremes of +12/-14 which means we cover all // possible day + hour combos (partials would be inside those extremes), so one // of them must match: we just need to add or subtract from it isPartial = true break } } } catch(e) { console.log(e+'') } } if (isPartial) { // calculate minute diff in 15's, ignore seconds if ('UTC' == offset) {offset = 'GMT+0'} let sign = offset.slice(3,4) let offsetHrs = offset.slice(4,offset.length) * 1 let expectedMins = oData.raw[key].slice(14,16) * 1 let partialMins = value.slice(14,16) * 1 let diff = Math.round((expectedMins - partialMins) / 15) * 15 let newoffset /* console.log(oData.raw[key], key) console.log(value) console.log(sign, offsetHrs, expectedMins, partialMins, diff) //*/ if ('+' == sign) { if (diff < 0) { newoffset = 'GMT' + sign + ((offsetHrs - 1)+'').padStart(2, '0') +':' + (60 + diff) } else { newoffset = 'GMT' + sign + (offsetHrs+'').padStart(2, '0') +':' + diff } } else { // negative sign we only have ±30 diffs. The code below _should_ handle ±15/±45 if (diff < 0) { newoffset = 'GMT' + sign + (offsetHrs+'').padStart(2, '0') +':' + Math.abs(diff) } else { newoffset = 'GMT' + sign + ((offsetHrs - 1)+'').padStart(2, '0') +':' + (60 - diff) } } if (undefined !== newoffset) { oData.minutekey = key oData.offset[key] = newoffset } } return } function format_offset(str) { if (!str.includes('GMT')) {return str} // format en short timezonename into ±xx:xx format str = str.slice(3) let sign = str.slice(0,1), time = str.slice(1) /* toString example GMT1300 */ /* other examples ['GMT+9:30','GMT+12','GMT-1','GMT+0'] */ if (!time.includes(':') && 4 == time.length) { time = time.slice(0,2)+':'+time.slice(2) } let parts = time.split(':') let hrs = parts[0].padStart(2,'0') let mins = (undefined == parts[1] ? '00' : parts[1]) return sign + hrs +':'+ mins } function get_minutes(str) { if (undefined !== str) { if (undefined !== tznShort[str]) { str = tznShort[str] } else if (str.includes('GMT')) { str = format_offset(str) } let minutes = ((str.slice(1,3) * 1)*60) + (str.slice(4,6)*1) let sign = (str[0] == '+' ? (minutes == 0 ? '': '-') : '') minutes = minutes * ('-' == sign ? -1 : 1) return [minutes, (str == '+00:00' ? '' : '['+ str +']')] } else { return '' } } function get_month(src) { let oMonths = { Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06', Jul: '07', Aug: '08', Sep: '09', Oct: '10', Nov: '11', Dec: '12', } let month = oMonths[src] return (undefined == month ? 'xx' : month) } function get_values(runNo) { // short timeZoneName exposes a GMT string in ~75% of timezones // which allows us more truthy offsets to fall back to oData = {'_runNo': runNo, 'errors' : {}, 'format': {}, 'offset': {}, 'tampered': [], 'raw': {}} // reset let option = { day: '2-digit', month: '2-digit', year: 'numeric', hour12: false, hour: '2-digit', minute: 'numeric', second: 'numeric', timeZoneName: 'short', timeZone: undefined, } let formatter = new Intl.DateTimeFormat('en', option) // changing option does not affect our formatter let id = 'iframelastmod' testdate = new Date() // get values methods.forEach(function(k){ let value try { // DTF if ('timeZone' == k) { value = formatter.format(testdate).replace(',','') // last modified } else if ('exslt' == k) { let xsltProcessor = new XSLTProcessor xsltProcessor.importStylesheet(doc) // fragment sticky datetime is set here let fragment = xsltProcessor.transformToFragment(doc, document) // toFragment is faster than toDocument value = fragment.childNodes[0].nodeValue } else if ('iframe' == k) { let el = document.createElement('iframe') el.setAttribute('id', id) document.body.appendChild(el) let target = el.contentDocument value = target.lastModified // contentWindow.document } else if ('parseFromString' == k) { value = (new DOMParser).parseFromString('','text/html').lastModified } else if ('parseHTMLUnsafe' == k) { value = Document.parseHTMLUnsafe('').lastModified // temporal } else if ('plainDateISO' == k) { value = Temporal.Now.plainDateISO().toString() } else if ('plainDateTimeISO' == k) { value = Temporal.Now.plainDateTimeISO().toString() } else if ('plainTimeISO' == k) { value = Temporal.Now.plainTimeISO().toString() value = value.slice(0,8) } else if ('zonedDateTimeISO' == k) { value = Temporal.Now.zonedDateTimeISO().toString() // to*string standalone } else if ('toLocaleDateString' == k) { option.timeZone = undefined value = testdate.toLocaleDateString('en', option).replace(',','') } else if ('toLocaleString' == k) { option.timeZone = undefined value = testdate.toLocaleString('en', option).replace(',','') } else if ('toLocaleTimeString' == k) { value = testdate.toLocaleTimeString('en', option).replace(',','') } else if ('toTimeString' == k) { let parts = testdate.toTimeString().split(' ') value = parts[0] +' '+ parts[1] // no formatting } else if ('toDateString' == k) { // https://searchfox.org/firefox-main/source/js/src/tests/test262/built-ins/Date/prototype/toDateString/format.js // dateRegExp = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{2} [0-9]{4}$/ // gecko format: e.g. "Fri Mar 20 2026" | blink seems to be the same, tested a few locales+timezone mixes let parts = testdate.toDateString().split(' ') value = get_month(parts[1]) +'/'+ parts[2] +'/'+ parts[3] } else if ('toString' == k) { // note: contents of the string from toString() are implementation-dependent // using formatter (DTF) defeats the purpose of the test // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toString // "it joins the string representation specified in toDateString() and toTimeString()" let parts = testdate.toString().replace(',','').split(' ') value = get_month(parts[1]) +'/'+ parts[2] +'/'+ parts[3] +' '+ parts[4] if (undefined !== parts[5]) {value += ' '+ parts[5]} } //if ('iframe' !== k) {foo++} // simulate only getting offset from a non-exslt lastMod // typecheck let typeCheck = typeFn(value) if ('string' !== typeCheck) { throw zErrType + typeCheck } else { /* test if (!aLastMods.includes(k)) { let index = value.indexOf(':') if (-1 !== index) { // shift minutes let mins = value.slice(index +1, index +3) * 1 mins = mins + (mins < 30 ? 1 : -1) // we only need move 1 minute: always 2 digits value = value.slice(0, index +1) + mins + value.slice(index +3, value.length) // shift seconds let secs = value.slice(index +4, index +6) * 1 let secShift = 11 secs = secs + (secs < 30 ? secShift : secShift * -1) // always 2 digits secs = (secs+'').padStart(2,'0') // just in case value = value.slice(0, index +4) + secs + value.slice(index +6, value.length) } } //*/ oData.raw[k] = value } } catch(e) { // the error differs if the console is open vs closed if ('parseHTMLUnsafe' == k) { if (e.name == 'NS_ERROR_UNEXPECTED') {e = 'Error: Permission denied to access property \"lastModified\"'} } oData.errors[k] = e+'' } }) removeElementFn(id) // partials (date or time only) need the missing corresponding part // we use lastModified items so if partials are legit they can match try { let partialkey, dateString, timeString for (let i=0; i < aLastMods.length; i++) { let key = aLastMods[i]; if (undefined !== oData.raw[key]) {partialkey = key; break} } if (undefined !== partialkey) { oData['partialkey'] = partialkey dateString = oData.raw[partialkey] if ('exslt' !== partialkey) { dateString = dateString.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$1-$2') } timeString = dateString.slice(11,19) dateString = dateString.slice(0,10) let aPartial = ['toTimeString','plainTimeISO','toDateString','plainDateISO'] aPartial.forEach(function(item) { // if undefined we must have had an error already if (undefined !== oData.raw[item]) { if (item.includes('Time')) { oData.raw[item] = dateString +' '+ oData.raw[item] } else { oData.raw[item] = oData.raw[item] +' '+ timeString } } }) } } catch(e) { console.log(e) } // check validity and format for (const k of Object.keys(oData.raw)) { let formatted, src = oData.raw[k] if ('exslt' == k) { oData.offset[k] = src.slice(-6) formatted = src.slice(0,-10).replace('T',' ') } else if ('zonedDateTimeISO' == k || 'plainDateTimeISO' == k) { // we only want the first 19 chars formatted = src.slice(0,19).replace('T',' ') // remember offsets if ('zonedDateTimeISO' == k) { let end = src.indexOf('[') oData.offset[k] = src.slice(end - 6, end) } } else { formatted = src.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$1-$2') // leverage short timezonename let shortname = formatted.split(' ')[2] formatted = formatted.slice(0,19) if (undefined !== shortname) { if (shortname.includes('GMT') || undefined !== tznShort[shortname]) {oData.offset[k] = shortname} } } if (checkValidDate(k, formatted)) {oData.format[k] = formatted} } } function checkValidDate(method, value) { try { if (new Date(value) +'' == 'Invalid Date') { oData.errors[method] = 'Invalid Date: ' + value return false } return true } catch(e) { oData.errors[method] = e+'' return false } } function checkMatch(runNo) { // set minutekey // if we have one at least one lastModified exslt/iframe/parseFromString/parseHTMLUnsafe // and we have an offset that matches, then we know the real value let minutekey, checkkey let oCheck = {hasLastMod: [], hasOffset: []} aLastMods.forEach(function(item){if (undefined !== oData.format[item]) {oCheck.hasLastMod.push(item)}}) for (const k of Object.keys(oData.offset)) {oCheck.hasOffset.push(k)} for (const j of Object.keys(oCheck)) { oData[j] = [] let lookup = 'hasLastMod' == j ? 'format' : 'offset' let array = oCheck[j] array.forEach(function(item){if (undefined !== oData[lookup][item]) {oData[j].push(item)}}) } if (Object.keys(oData.hasLastMod).length && Object.keys(oData.hasOffset).length) { // last mods can't be tampered with if (oData.hasOffset.includes('exslt')) { minutekey = 'exslt' // if we have exslt offset we also have the format } else { let aOff = oData.hasOffset, aLast = oData.hasLastMod // loop valid items which have an offset for (let i = 0; i < aOff.length; i++) { if (undefined !== minutekey) {break} let offsetkey = aOff[i], got = oData.format[offsetkey] // loop valid lastModified items which should all be truthy for (let j = 0; j < aLast.length; j++) { let lastkey = aLast[j], expected = oData.format[lastkey] if (expected == got) {minutekey = offsetkey; break} } } } } // are they all the same let aTmp = [] for (const k of Object.keys(oData.format)) {aTmp.push(oData.format[k])} aTmp = dedupeArray(aTmp) if (undefined !== minutekey) { oData['minutekey'] = minutekey checkkey = minutekey } if (aTmp.length == 1) {return true} if (undefined == checkkey) { // just because we don't have a minutekey, doesn't mean we can't compare to a lastmodifed checkkey = oCheck.hasLastMod[0] } if (undefined == checkkey) {return aTmp.length == 1} oData['checkkey'] = checkkey // here is where we catch the tampering // some diffs + we have a checkkey which we consider truthy // compare the rest to checkkey (which we treat as truthy) // get checkkey parts let mValue = oData.format[checkkey], mParts = mValue.split(' '), mDate = mParts[0], mTime = mParts[1].split(':') for (const k of Object.keys(oData.format)) { let isDiff = false // reset each check, assume false if (k !== checkkey) { // exempt checkkey try { let kValue = oData.format[k], kParts = kValue.split(' '), kDate = kParts[0], kTime = kParts[1].split(':') // compare k to m(inutekey) if (kValue == mValue) { // perfect match } else if (kDate !== mDate) { // different date isDiff = true // this covers toDateString and plainDateISO //if (isFile && 2 == runNo) {console.log('date changed', k, kDate)} } else { // time diff only let tmpDiff = { h: (kTime[0] * 1) - (mTime[0] * 1), min: (kTime[1] * 1) - (mTime[1] * 1), s: (kTime[2] * 1) - (mTime[2] * 1), } // allow 10 seconds: jank, also leap seconds // abs !important to cover being ahead or behind let secondsDiff = Math.abs((tmpDiff.h * 3600) + (tmpDiff.min * 60) + tmpDiff.s) if (secondsDiff > 10) { isDiff = true //if (isFile && 2 == runNo) {console.log('secondsDiff', secondsDiff, k)} } } if (isDiff) {oData.tampered.push(k)} //METRIC +'_'+ k)} } catch(e) { console.log(e+'') } } } return 0 == oData.tampered.length } function display_values() { // all valid dates are in format, everything else is in errors for (const k of Object.keys(oData.format)) { let n = METRIC +'_'+ k, isLies = false, extra = '' let value = oData.format[k] // style + record lies if (oData.tampered.includes(k)) { isLies = true let tmpvalue = value // tidy up partial if ('toTimeString' == k || 'plainTimeISO' == k) {tmpvalue = tmpvalue.slice(-8) } else if ('toDateString' == k || 'plainDateISO' == k) {tmpvalue = tmpvalue.slice(0,10) } log_known(4, n, tmpvalue) } // tidy up partial if ('toTimeString' == k || 'plainTimeISO' == k) { value = value.slice(-8) dom[k +'spaces'] = ' '.repeat(10) } else if ('toDateString' == k || 'plainDateISO' == k) {value = value.slice(0,10)} // add extra display info let offsetStr = oData.offset[k] if (undefined !== offsetStr & 'string' == typeof offsetStr) {extra = ' '+ offsetStr} value += extra addDisplay(4, n, value,'','', isLies) } for (const k of Object.keys(oData.errors)) { addDisplay(4, METRIC +'_'+ k, log_error(4, METRIC +'_'+ k, oData.errors[k])) } } // run, try to get isMatch get_values(1) let isMatch = checkMatch(1) if (!isMatch) { // ~0.5 ms to grab our mods: 1 in 86,400 seconds tick over a day // so we'd have to be really unlucky to hit this: try again get_values(2) isMatch = checkMatch(2) } oData['isMatch'] = isMatch display_values() if (undefined == oData.minutekey) {create_offset()} // after display so we don't add offsets to a lastmod let finalvalue, finaldisplay, utcValue let errCount = Object.keys(oData.errors).length let tamperCount = oData.tampered.length if (undefined !== oData.minutekey) { let finaldata = get_minutes(oData.offset[oData.minutekey]) finalvalue = finaldata[0] finaldisplay = finaldata.join(' ') utcValue = oData.format[oData.minutekey] if (0 == tamperCount && 0 == errCount) { // no lies + no errors notation = tz_green } else if (gRun) { // health lookup let aHealth = [] if (errCount > 0) {aHealth.push(errCount + ' error' + (errCount == 1 ? '' : 's'))} if (tamperCount > 0) {aHealth.push(tamperCount + ' mismatch' + (tamperCount == 1 ? '' : 'es'))} if (gRun) {sDetail[isScope].lookup[METRIC] = aHealth.join(' | ')} } addDisplay(4, METRIC, finaldisplay,'', notation) addData(4, METRIC, finalvalue) } else { // all errors if (methods.length == errCount) { addBoth(4, METRIC, zErr +'s','', notation, zErr) } else { // 4 scenarios with isMatch/isTamper // if no checkkey then we never compared and isMatch is default false let isTamper = tamperCount > 0 finalvalue = (isMatch || undefined == oData.checkkey) ? 'unknown' : 'mixed' addBoth(4, METRIC, finalvalue,'', notation,'', isTamper) } } //if (isFile) {console.log('timezone offset\n', oData)} log_perf(4, METRIC, t0) return {'nowValue': finalvalue, 'utcValue': utcValue, 'tampered': oData.tampered} } function get_timezone_offsets(METRIC, nowValue, utcValue) { let t0 = nowFn(), notation = tz_red let years = [1879, 1952, 1976, 2025] let days = { // to make sure we don't change years or months when a day or two ticks over // use the 15th - this makes get* and getUTC* PoCs possible 'January 15': {numbers: [1,15], str :'01-15'}, 'July 15': {numbers: [7,15], str: '07-15'} } let aMethods = [ //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_components_and_time_zones 'components','components_utc','date','date.parse','date.valueOf','getTime', 'getTimezoneOffset','offsetNanoseconds','timeZoneName','Symbol.toPrimitive', ] let oMultiplier = { // year + month: our test dates are middle of the month: legit components will never differ // fake values that differ we don't need to be precise (because they're garbage) so no nee to // computer variable days in years and months, we can just use constants 365 + 31 '1': 60000 * 60 * 24 * 365, // year '2': 60000 * 60 * 24 * 31, // month '3': 60000 * 60 * 24, // day '4': 60000 * 60, // hour '5': 60000, // minute '6': 1000, // second '7': 1, // ms } let tznShort = { // hardcoded AKST: 540, AKDT: 480, AST: 240, ADT: 180, CST: 360, CDT: 300, EST: 300, EDT: 240, HAST: 600, HADT: 540, HST: 600, HDT: 540, MST: 420, MDT: 360, PST: 480, PDT: 420, UTC: 0, GMT: 0, 'GMT+0': 0, } let oList = {} years.forEach(function(year) {oList[year] = days}) // if we know the real current offset (nowValue) we can add a silent non-collected // now datetime and compare it's value to nowValue to determine if tampered if ('number' == typeFn(nowValue)) {oList['now'] = {'now': ''}} let oData = {'calc': {}, 'display': {}, 'errors': {}, 'hashes': {'all': {}, 'valid': {}}, 'lies': {}, 'math': {'1.utc': {}, '2.timezone': {}}, 'now': {}, 'numbers': {}, 'utc': {} } aMethods.forEach(function(method) { oData.calc[method] = {} years.forEach(function(year) {oData.calc[method][year] = []}) }) let tznOption = { day: '2-digit', month: '2-digit', year: 'numeric', hour12: false, hour: '2-digit', minute: 'numeric', second: 'numeric', timeZoneName: 'short', } let oExpected = { '1879-01-15': {'components': '1879 0 15', 'other': -2870420400000}, '1879-07-15': {'components': '1879 6 15', 'other': -2854782000000}, '1952-01-15': {'components': '1952 0 15', 'other': -566823600000}, '1952-07-15': {'components': '1952 6 15', 'other': -551098800000}, '1976-01-15': {'components': '1976 0 15', 'other': 190558800000}, '1976-07-15': {'components': '1976 6 15', 'other': 206283600000}, '2025-01-15': {'components': '2025 0 15', 'other': 1736946000000}, '2025-07-15': {'components': '2025 6 15', 'other': 1752584400000}, } try { let formatter = new Intl.DateTimeFormat('en', tznOption) for (const year of Object.keys(oList)) { let isNow = 'now' == year Object.keys(oList[year]).forEach(function(day) { let isFirst = (year == years[0] && day == 'January 15') let test, control, key if (!isNow) { let datetime = day +', '+ year +' 13:00:00' control = new Date(datetime +' UTC') test = new Date(datetime) key = year +'-'+ days[day].str oData.math['1.utc'][key] = {}; oData.math['2.timezone'][key] = {}; } else { // if we have a nowValue, we had a minutekey and a formatted string test = new Date(utcValue) control = new Date(utcValue +' UTC') } if (runSE) {foo++} else if (runST) {test = NaN} aMethods.forEach(function(method) { if (undefined == oData.errors[method]) { let offset, k = 60000, oDiffs, utc, time try { if ('getTimezoneOffset' == method) { offset = test.getTimezoneOffset() k = 1 } else if ('timeZoneName' == method) { // it doesn't really matter what method we use since they're all exposed elsewhere let tznDate = formatter.format(test).replace(',','') time = tznDate.split(' ')[2] k = 1 if (undefined !== tznShort[time]) { offset = tznShort[time] } else { if ('GMT' !== time.slice(0,3)) {throw zErrInvalid + time} // hrs, minutes, seconds let sign = time.includes('-') ? 1 : -1 let value = time.replace('GMT','') value = value.replace('-','') value = value.replace('+','') let parts = value.split(':') offset = parts[0] * 60 if (undefined !== parts[1]) { offset += parts[1] * 1} // minutes if (undefined !== parts[2]) { offset += (parts[2] * 1)/60} // seconds offset = offset * sign } } else { if ('date.parse' == method) { time = Date.parse(test); utc = Date.parse(control) } else if ('date.valueOf' == method) { time = test.valueOf(); utc = control.valueOf() } else if ('Symbol.toPrimitive' == method) { time = test[Symbol.toPrimitive]('number') utc = control[Symbol.toPrimitive]('number') offset = time - utc } else if ('getTime' == method) { time = test.getTime(); utc = control.getTime() } else if ('date' == method) { utc = control * 1; time = test * 1 } else if ('offsetNanoseconds' == method) { // instant: YYYY-MM-DD T HH:mm:ss.sssssssss Z/±HH:mm [time_zone_id] // e.g. 1879-01-01T13:00Z let tzid = Temporal.Now.timeZoneId() let instantStr = isNow ? utcValue +'Z' : year +'-'+ days[day].str +'T13:00Z' let instant = Temporal.Instant.from(instantStr) time = instant.toZonedDateTimeISO(tzid).offsetNanoseconds // UTC is always zero so we could hard-code this // BUT it's nice to catch any fuckery caqused by extensions utc = instant.toZonedDateTimeISO('UTC').offsetNanoseconds offset = (utc - time) / 1e6 //if (!isNow) (offset = offset * 2) // mixed but no lies } else if ('components' == method) { oDiffs = { '1': [test.getUTCFullYear(), control.getUTCFullYear()], '2': [test.getUTCMonth(), control.getUTCMonth()], '3': [test.getUTCDate(), control.getUTCDate()], '4': [test.getUTCHours(), control.getUTCHours()], '5': [test.getUTCMinutes(), control.getUTCMinutes()], '6': [test.getUTCSeconds(), control.getUTCSeconds()], '7': [test.getUTCMilliseconds(), control.getUTCMilliseconds()], } offset = 0, utc = [], time = [] for (const k of Object.keys(oDiffs)) { offset += (oMultiplier[k] * (oDiffs[k][0] - oDiffs[k][1])) utc.push(oDiffs[k][1]) // control time.push(oDiffs[k][0]) // test } utc = utc.join(' '); time = time.join(' ') } else if ('components_utc' == method) { oDiffs = { '1': [test.getFullYear(), control.getFullYear()], '2': [test.getMonth(), control.getMonth()], '3': [test.getDate(), control.getDate()], '4': [test.getHours(), control.getHours()], '5': [test.getMinutes(), control.getMinutes()], '6': [test.getSeconds(), control.getSeconds()], '7': [test.getMilliseconds(), control.getMilliseconds()], } offset = 0, utc = [], time = [] for (const k of Object.keys(oDiffs)) { // this is reversed: we subtract time from utc offset += (oMultiplier[k] * (oDiffs[k][0] - oDiffs[k][1])) utc.push(oDiffs[k][0]) // reversed so we use test time.push(oDiffs[k][1]) // control } utc = utc.join(' '); time = time.join(' ') } } let isTZN = 'timeZoneName' == method if (undefined == offset) {offset = time - utc} if (isNow) { oData.now[method] = offset/k } else { if (undefined !== utc) { let expected, isUTCMatch = true, isPartial = false oData.math['1.utc'][key][method] = utc oData.math['2.timezone'][key][method] = time // check for utc tampering if ('1879' == year &&'date.parse' == method) { expected = oExpected[key].other // only old-timey years have partial minutes and only partial minutes are offset from expected if (Number.isInteger(offset/k)) { isUTCMatch = utc == expected } else { // can't match expected (0 diff) and diff within 60000 isPartial = true let diff = Math.abs(expected - utc) isUTCMatch = diff < 60000 && diff !== 0 } } else { if ('offsetNanoseconds' == method) {expected = 0 } else if (method.includes('components')) {expected = oExpected[key].components +' 13 0 0 0' } else {expected = oExpected[key].other} if (undefined !== expected) {isUTCMatch = utc == expected} } // log tampering if (!isUTCMatch) { oData.lies[method] = ['utc'] if (undefined == oData.utc[method]) {oData.utc[method] = {}} oData.utc[method][key] = ['expected'+ (isPartial ? ' ±60000' : ''), expected, 'got', utc] } } if (isTZN) { oData.math['2.timezone'][key][method] = time } if (isFirst) { let typeCheck = typeFn(offset) //console.log(method, typeCheck, offset) if ('number' !== typeCheck) {throw zErrType + typeCheck} } oData.calc[method][year].push(offset/k) } } catch(e) { oData.errors[method] = log_error(4, METRIC +'_'+ method, e) delete oData.calc[method] delete oData.lies[method] delete oData.utc[method] } } }) }) } } catch(e) { addBoth(4, METRIC, log_error(4, METRIC, e),'', notation, zErr) return {'health': false, 'hash': zErr} } // display errors for (const k of Object.keys(oData.errors)) {addDisplay(4, METRIC +'_'+ k, oData.errors[k])} // exit if all errors if (Object.keys(oData.errors).length == aMethods.length) { addBoth(4, METRIC, zErr +'s', '', notation, zErr) return {'health': false, 'hash': zErr} } for (const k of Object.keys(oData.calc)) { let tmpDisplay = [] for (const y of Object.keys(oData.calc[k])) { oData.calc[k][y] = dedupeArray(oData.calc[k][y]) tmpDisplay.push(oData.calc[k][y].join(', ')) } // out of 339 unique results: 1 = 57 chars, 1 = 52 chars .. the rest are all 50 and under // will likely cause line overflow on android but it's cleaner to manage and visually see let str = '' if (isDesktop && undefined !== oData.now[k]) {str = s99 +' ('+ oData.now[k] +')'+ sc} oData.display[k] = tmpDisplay.join(' | ') + str if (undefined !== oData.now[k]) { if (oData.now[k] !== nowValue) { if (undefined == oData.lies[k]) {oData.lies[k] = []} oData.lies[k].push('now') log_known(4, METRIC +'_'+ k, tmpDisplay.join(' | ')) } } addDisplay(4, METRIC +'_'+ k, oData.display[k], '','', undefined !== oData.lies[k]) let hash = mini(oData.calc[k]) if (undefined == oData.hashes.all[hash]) {oData.hashes.all[hash] = [k]} else {oData.hashes.all[hash].push(k)} if (undefined == oData.lies[k]) { if (undefined == oData.hashes.valid[hash]) {oData.hashes.valid[hash] = [k]} else {oData.hashes.valid[hash].push(k)} } } // summarize oData.math etc // add mismatches if (Object.keys(oData.utc).length) { oData.numbers['0.utc_tampered'] = {} for (const k of Object.keys(oData.utc).sort()) { oData.numbers['0.utc_tampered'][k] = oData.utc[k] } } for (const type of Object.keys(oData.math)) { // don't include any data from items that eventually errored let tmpobj = {}, newobj = {} for (const d of Object.keys(oData.math[type])) { newobj[d] = {} for (const k of Object.keys(oData.math[type][d])) { if (undefined == oData.errors[k]) { let itemdata = oData.math[type][d][k], itemhash = mini(itemdata) +' ' if (undefined == newobj[d][itemhash]) { newobj[d][itemhash] = {'data': itemdata, 'group': [k]} } else {newobj[d][itemhash].group.push(k)} } } } for (const d of Object.keys(newobj)) { tmpobj[d] = {} for (const k of Object.keys(newobj[d])) {tmpobj[d][newobj[d][k].group.join(' ')] = newobj[d][k].data} } oData.numbers[type] = tmpobj } // OFFSETS math data // we only need to check for any utc methods: note: math data and lies + utc are only recorded for methods that didn't error let btnColor = 4 if (isGecko) {btnColor = Object.keys(oData.utc).length ? 'bad' : 'good'} addDisplay(4, METRIC +'_data', addButton(btnColor, METRIC +'_data', 'data')) sDetail[isScope][METRIC +'_data'] = oData.numbers // health lookup let errCount = Object.keys(oData.errors).length let tamperCount = Object.keys(oData.lies).length if (gRun && errCount + tamperCount > 0) { let aHealth = [] if (errCount > 0) {aHealth.push(errCount + ' error' + (errCount == 1 ? '' : 's'))} if (tamperCount > 0) {aHealth.push(tamperCount + ' mismatch' + (tamperCount == 1 ? '' : 'es'))} if (gRun) {sDetail[isScope].lookup[METRIC] = aHealth.join(' | ')} } // summarize let hash, data ='', btn ='', isLies = false let isMixed = Object.keys(oData.hashes.all).length > 1 let isValid = Object.keys(oData.hashes.valid).length == 1 if (isValid) { // we may have lies, but we also have a single valid (non-lie) hash for (const h of Object.keys(oData.hashes.valid)) { // there's only one hash let m = oData.hashes.valid[h][0] // get first method listed hash = h; data = oData.calc[m] btn = addButton(4, METRIC) // notation: no errors, no lies, i.e our single valid hash holds all methods if (oData.hashes.valid[h].length == aMethods.length) {notation = tz_green} } } else { // it may be feasible no lies detected but we have mixed results == clearly someone is lying isLies = tamperCount > 0 || isMixed if (isMixed) { hash = 'mixed' } else { for (const h of Object.keys(oData.hashes.all)) { // there's only one hash let m = oData.hashes.all[h][0] // get first method listed hash = h; data = oData.calc[m] btn = addButton(4, METRIC) } } } addBoth(4, METRIC, hash, btn, notation, data, isLies) //if (isFile) {console.log('timezone offsets\n', oData)} log_perf(4, METRIC, t0) // return health as true if no errors and no lies and only one valid hash for all methods return {'health': notation == tz_green, 'hash': hash} } /* l10n */ function get_l10n_css(METRIC) { if (!isGecko) {addBoth(4, METRIC, zNA); return} let hash, data = '', notation = '' //isLanguageSmart ? locale_red : '' try { } catch(e) { hash = e; data = zErrLog } addBoth(4, METRIC, hash,'', notation) return } const get_l10n_media_messages = (METRIC) => new Promise(resolve => { if (!isGecko) {addBoth(4, METRIC, zNA); return resolve()} // https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/layout/MediaDocument.properties let hash, btn='', data = {}, notation = isLanguageSmart ? locale_red : '' let aList = ['InvalidImage','ScaledImage'] for (const k of aList) { let target = dom['tzp'+ k], title ='' try { if ('ScaledImage' == k) { title = target.contentWindow.document.title title = title.replace(k +'.png', '') // strip image name to reduce noise } else { const image = target.contentWindow.document.querySelector('img') title = image.alt title = title.replace(target.src, '') // remove noise } data[k] = title.trim() } catch(err) { log_error(4, METRIC +'_'+ k, err) data[k] = zErr } } hash = mini(data); btn = addButton(4, METRIC) if (isLanguageSmart) { if (isLocaleValid && localesSupported[isLocaleAlt] !== undefined) { if (localesSupported[isLocaleAlt].m.includes(hash)) {notation = locale_green} } } addBoth(4, METRIC, hash, btn, notation, data) return resolve() }) function get_l10n_parsererror_direction(METRIC) { //if (!isGecko) {addBoth(4, METRIC, zNA); return} // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#error_handling // 1954813: // 1666613: currently relies on chrome://global/locale/intl.css let value, data = '', notation = isLanguageSmart ? locale_red : '' try { if (isGecko && isVer > 146) { // 1666613: no need to touch the dom in gecko: 0.17ms let parser = (new DOMParser()).parseFromString('INVALID', 'text/xml') value = parser.firstChild.attributes[0].nodeValue } else { // 0.23ms let target = dom.tzpDirection target.innerHTML = '<parsererror></parsererror>' value = getComputedStyle(target.children[0]).direction } // check if (runST) {value = ''} else if (runSI) {value = 'upsidedown'} let typeCheck = typeFn(value) if ('string' !== typeCheck) {throw zErrType + typeCheck} let aGood = ['ltr','rtl'] if (!aGood.includes(value)) {throw zErrInvalid +'expected '+ aGood.join(', ') +': got '+ value} // notation // since this is just BB (or FF en-US), we know only three locales are rtl: ar, fa, he if (isLanguageSmart && isLocaleValid) { let aRTL = ['ar','fa','he'] let expected = aRTL.includes(isLocaleValue) ? 'rtl' : 'ltr' if (expected == value) {notation = locale_green} } } catch(e) { value = e; data = zErrLog } addBoth(4, METRIC, value,'', notation, data) return } const get_l10n_reporting_messages = (METRIC) => new Promise(resolve => { // https://developer.mozilla.org/en-US/docs/Web/API/Reporting_API // dom.reporting.enabled // note: if/when the API is enabled, BB alpha can differ as deprecation warnings change // since we use isLanguageSmart (which can include non isBBESR), it's not worth coding // around that to remove false positives - we don't care about BB alpha health let t0 = nowFn() function exit(res) { try {observer.disconnect()} catch(e) {} if ('string' == typeFn(res)) { // undefined, n/a, errors hash = res } else { if (hasReporting) { data = isReporting } else { //console.log(res) // get up to x unique deprecated messages let max = 10 let aSet = new Set() for (let i=0; i < res.length; i++) { let msg = res[i].body.message msg = msg.replace('https://developer.mozilla.org/docs/Web/API/Element/releasePointerCapture','').trim() aSet.add(msg) if (max == aSet.size) {break} // reruns accrue messages so break } data = Array.from(aSet).sort() isReporting = data // cache the result for reruns } if (data.length) { hash = mini(data); btn = addButton(4, METRIC) // + (hasReporting ? ' [cached]' : ' [generated]') } else { hash = 'none' } // notate if (isLanguageSmart) { if (isLocaleValid && localesSupported[isLocaleAlt] !== undefined) { let check = localesSupported[isLocaleAlt].r // if blank then it hasn't been translated yet if ('' == check) {check = localesSupported['en-US'].r} if (check.includes(hash)) {notation = locale_green} } } } addBoth(4, METRIC, hash, btn, notation, data) if (!hasReporting) {log_perf(4, METRIC, t0)} return resolve() } // shipped in FF149+ 1976074 // note: we don't need to notate for BB if the API is enabled or not, as that's covered by window properties let hash, data ='', btn ='', notation = '', observer let hasReporting = 'array' == typeFn(isReporting, true) if (!isGecko) { exit(zNA) } else if (undefined == isReporting && undefined == window.ReportingObserver) { exit('undefined') } else { // but we do notate when it is on to match locale notation = isLanguageSmart ? locale_red : '' if (hasReporting) { exit() } else { try { if (runSE) {foo++} observer = new ReportingObserver((reports, observer) => {exit(reports)}, {types: ['deprecation'], buffered: true}) observer.observe() } catch(e) { data = zErrLog; exit(e+'') } } } }) function get_l10n_validation_messages(METRIC) { // https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/dom/dom.properties const aNames = ['BadInputNumber','CheckboxMissing','DateTimeRangeOverflow','DateTimeRangeUnderflow', 'FileMissing','InvalidEmail','InvalidURL','NumberRangeOverflow','NumberRangeUnderflow', 'PatternMismatch','RadioMissing','SelectMissing','StepMismatch','ValueMissing',] const input = "<input type='number' required>" + "<input type='checkbox' required>" + "<input type='date' value='2024-01-01' max='2023-12-31'>" + "<input type='date' value='2022-01-01' min='2023-12-31'>" + "<input type='file' required>" + "<input type='email' value='a'>" + "<input type='url' value='a'>" + "<input type='number' max='1974.3' value='2000'>" + "<input type='number' min='8026.5' value='1'>" + "<input type='tel' pattern='[0-9]{1}' value='a'>" + "<input type='radio' required name='radiogroup'>" + "<select required><option></option></select>" + "<input type='number' min='1.2345' step='1005.5545' value='2'>" + "<input type='text' required>" let hash, btn ='', data = {}, notation = isLanguageSmart ? locale_red : '' try { let collection = ((new DOMParser).parseFromString(input, 'text/html')).body.children let cType = typeFn(collection) if ('object' !== cType) {throw zErrType + cType} for (const k of Object.keys(collection)) { let msg = collection[k].validationMessage if (runST) {msg = undefined} let typeCheck = typeFn(msg) if ('string' !== typeCheck) {throw zErrType + typeCheck} data[aNames[k]] = msg } hash = mini(data) let count = Object.keys(data).length let details = count === aNames.length ? 'details' : count +'/' + aNames.length btn = addButton(4, METRIC, details) if (isLanguageSmart) { if (isLocaleValid && localesSupported[isLocaleAlt] !== undefined) { if (localesSupported[isLocaleAlt].v.includes(hash)) {notation = locale_green} } } } catch(e) { hash = e; data = zErrLog } addBoth(4, METRIC, hash, btn, notation, data) return } function get_l10n_xml_messages(METRIC) { // https://searchfox.org/firefox-main/source/dom/locales/en-US/chrome/layout/xmlparser.properties let hash, btn ='', data = isXML, notation = isLanguageSmart ? locale_red : '' if ('string' == typeof isXML) { hash = isXML; data = isXML == zNA ? '' : zErrLog } else { hash = mini(isXML); btn = addButton(4, METRIC) if (isLanguageSmart) { if (isLocaleValid && localesSupported[isLocaleAlt] !== undefined) { if (localesSupported[isLocaleAlt].x.includes(hash)) {notation = locale_green} } } } addBoth(4, METRIC, hash, btn, notation, data) return } function get_l10n_xml_prettyprint(METRIC, isLies) { if (!isGecko) {addBoth(4, METRIC, zNA); return} // https://searchfox.org/firefox-main/source/dom/locales/en-US/dom = XMLPrettyPrint // note file schema errors due to CORS // by using a narrow iframe width, word segmentation line breaks determine the height, // and the content varies per app locale. It's imperative that the iframe be very // narrow (TZP uses 20px) as this ensure all scripts return the maximum number of lines // Deterministic health checks can't be hardcoded due to subpixels (system + other scaling) // and fonts (per platform + language), but we could simulate + compare let value, data ='', notation='' try { if (gRun) {dom.tzpXMLunstyled.width = 20} // ensure narrow width for max lines let target = dom.tzpXMLunstyled.contentDocument.firstChild let method = measureFn(target, METRIC) if (undefined !== method.error) {throw method.errorstring} value = method.height } catch(e) { value = e; data = zErrLog } // if the xml isn't loaded in time we will get a low default value (e.g. 0 latin, 8 arabic, 18 privacyX) // notate this as well as unexpected errors (i.e value is a string) // don't notate if file schema (it makes file vs http have different health counts which is not good) // this test is gecko only, so we don't need to check isLanguageSmart if (!isFile) { if ('number' !== typeof value || value < 50) {notation = isLanguageSmart ? locale_red : default_red} } addBoth(4, METRIC, value,'', notation, data, isLies) } function get_l10n_xslt_messages(METRIC) { if (!isGecko) {addBoth(4, METRIC, zNA); return} // https://searchfox.org/firefox-main/source/dom/locales/en-US/dom/xslt.ftl // note file schema errors due to CORS // we only need the one test for max entropy (tested Base Browser) // but we need an object to create a btn, and this also allows future expansion // FF151: dom.xslt.enabled // we're only loading the iframe once (on page load). The pref dictates if the xslt was // parsed and an error displayed or not - and that's not going to change if the pref is // toggled a rerun done let hash, data ='', btn='', notation = isLanguageSmart ? locale_red : '' try { let msg = dom.tzpXSLT.contentDocument.children[0].textContent if ('a' == msg) { // ToDo: cleanup notation when this becomes the standard hash = zD // XSLT disabled on page load } else { data = {'xslt-parse-failure': msg} hash = mini(data); btn = addButton(4, METRIC) } } catch(e) { hash = e; data = zErrLog } if (isLanguageSmart) { if (isLocaleValid && localesSupported[isLocaleAlt] !== undefined) { if (localesSupported[isLocaleAlt].xs.includes(hash)) {notation = locale_green} } } addBoth(4, METRIC, hash, btn, notation, data) } function get_l10n_xslt_sort(METRIC) { if (!isGecko) {addBoth(4, METRIC, zNA); return} // 1978383: xsl:sort uses only base sensitivity / primary strength // so we can't use plural rules to get a determinsitic result let hash, btn ='', data = {}, notation = isLanguageSmart ? locale_red : '' if (!isXSLT) { addBoth(4, METRIC, zD,'', notation); return } try { if (runSE) {foo++} // get characters let aSource = oIntlLocale.collation.sort, aChars = [], aData = [] aSource.forEach(function(item){aChars.push(item)}) aChars.sort() // always sort to match char array same as collation poc // build xslt aChars.forEach(function(item){aData.push('<a>'+ item +'</a>')}) const xData = '<?xml version="1.0" encoding="UTF-8"?><doc>'+ aData.join('') +'</doc>' const xslText = '<?xml version="1.0" encoding="UTF-8"?>' +'<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' +'<xsl:template match="/"><xsl:for-each select="doc/a"><xsl:sort select="text()"/>' +'<xsl:value-of select="text()"/>,</xsl:for-each></xsl:template></xsl:stylesheet>' // run xslt const parser = new DOMParser() const xsltProcessor = new XSLTProcessor() const xslStylesheet = parser.parseFromString(xslText, "application/xml") xsltProcessor.importStylesheet(xslStylesheet) const xmlDoc = parser.parseFromString(xData, "application/xml") const styledDoc = xsltProcessor.transformToDocument(xmlDoc) let aTmp = styledDoc.firstChild.textContent.split(/[\s,\n]+/); aTmp = aTmp.slice(0, -1) let dataStr = (aTmp.join(' , ')).trim() data = {'sort': dataStr} hash = mini(data); btn = addButton(4, METRIC) if (isLanguageSmart) { if (isLocaleValid && localesSupported[isLocaleAlt] !== undefined) { // compare the string hash if (localesSupported[isLocaleAlt].xsort.includes(mini(dataStr))) {notation = locale_green} } } } catch(e) { hash = e; data = zErrLog } addBoth(4, METRIC, hash, btn, notation, data) return } /* TODO */ const get_dates = () => new Promise(resolve => { let d = new Date(Date.UTC(2023, 0, 1, 0, 0, 1)) // // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat // locale options let o = { // numeric or 2-digit: always use numeric year: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', // true or false: true override hourCycle and forces h11 or h12 (locale dependent) == AM/PM hour12: true, // long short narrow era: 'long', month: 'long', // also numeric or 2-digit but we already have that covered weekday: 'long', // long short // NOTE: FF91+ longGeneric, longOffset, shortGeneric, shortOffset // see timezonename PoC: tested july 2025: long, longOffset, shortOffset add nothing // short vs shortGeneric is a single extra unique hash // long matches longGeneric in terms of entropy // use long/short for old-timey support timeZoneName: 'long' } // max option combos would be 3 x 3 x 3 x 2 = 54 // then x dates and x calendars // that's a lot of tests let localecode = undefined let DTFo try {DTFo = Intl.DateTimeFormat(undefined, o)} catch {} function get_item(item) { let itemPad = 'item '+ item try { // STRINGS if (item == 1) {return d.toTimeString() } else if (item == 2) {return d // a date object: default format? } else if (item == 3) {return d.toString() // redundant? // options } else if (item == 4) {return d.toLocaleString(localecode, o) } else if (item == 5) {return d.toLocaleDateString(localecode, o) } else if (item == 6) {return d.toLocaleTimeString(localecode, o) // no options } else if (item == 7) {return d.toLocaleString(localecode) } else if (item == 8) {return [d].toLocaleString(localecode) // typed array } else if (item == 9) {return d.toLocaleDateString(localecode) } else if (item == 10) {return d.toLocaleTimeString(localecode) // DTF } else if (item == 11) {return DTFo.format(d) } else if (item == 12) { let f = Intl.DateTimeFormat(localecode, o) let temp = f.formatToParts(d) return temp.map(function(entry){return entry.value}).join('') } else if (item == 13) {return Intl.DateTimeFormat().format(d) } else if (item == 14) { // FF91+: 1710429 // note: use hour12 - https://bugzilla.mozilla.org/show_bug.cgi?id=1645115#c9 // FF91: extended TZNs are type "unknown" let tzRes = [] try { let tzNames = ['longGeneric','shortGeneric'] let tzDays = [d] let tz tzDays.forEach(function(day) { tzNames.forEach(function(name) { tz ='' try { let formatter = Intl.DateTimeFormat(localecode, {hour12: true, timeZoneName: name}) tz = formatter.format(day) } catch(e) { if (day == tzDays[0]) { log_error(4, itemPad +': '+ name, e) } tz = zErr } tzRes.push(tz) }) }) return tzRes.join(' | ') } catch(e) { log_error(4, itemPad +': timeZoneName', e) return zErr } } else if (item == 15) { // FF91+: 1653024: formatRange let date1 = new Date(Date.UTC(2020, 0, 15, 11, 59, 59)), date2 = new Date(Date.UTC(2020, 0, 15, 12, 0, 1)), date3 = new Date(Date.UTC(2020, 8, 19, 23, 15, 30)) return DTFo.formatRange(date1, date2) +' | '+ DTFo.formatRange(date1, date3) } else { return zSKIP } } catch(e) { log_error(4, itemPad, e) return zErr } } for (let i=1; i < 16; i++) { let result = get_item(i) if (result !== zSKIP) { let typeExpected = 2 == i ? 'empty object' : 'string' let typeCheck = typeFn(result) if (typeExpected !== typeCheck) {result = zErrType + typeCheck} addDisplay(4, 'ldt'+ i, result) } } return resolve() }) const outputRegion = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('region')) {return resolve()} oIntlLocalePerf = {} // reset set_isLanguageSmart() // required for BB health in get_language_locale() Promise.all([ get_geo('geolocation'), get_language_locale(), // sets isLocaleValid/Value, isLanguagesNav ]).then(function(){ // add smarts if locale matches: i.e we can notate messages for FF // isLanguageSmart controls health for l10n (and language/locale but we also check isBB in those) if (isGecko && !isLanguageSmart && isSmart && isDesktop) { // this becomes problematic to maintain for all those 40+ locales over a full ESR cycle as translations // change or deprecated warnings etc come and go: the health check only really matters if you're spoofing // en-US anyway, so let's limit to en-US for non-BB to avoid non-BB false positivess if ('en-US' == isLocaleValue) { if (localesSupported[isLocaleValue] !== undefined) {isLanguageSmart = true} } } let isLies = isDomRect == -1 Promise.all([ get_language_system('languages_system'), // uses isLanguagesNav get_locale_intl(), get_timezone('timezone'), // sets isTimeZoneValid/Value get_l10n_validation_messages('l10n_validation_messages'), get_l10n_xml_messages('l10n_xml_messages'), get_l10n_parsererror_direction('l10n_parsererror_direction'), get_l10n_xslt_sort('l10n_xslt_sort'), get_l10n_xml_prettyprint('l10n_xml_prettyprint', isLies), get_l10n_xslt_messages('l10n_xslt_messages'), //get_l10n_css('l10n_css'), ]).then(function(){ Promise.all([ get_dates_intl(), // uses isTimeZoneValid/Value + isLocaleValid/Value get_dates(), // to migrate to get_dates_intl get_l10n_reporting_messages('l10n_reporting_messages'), get_l10n_media_messages('l10n_media_messages'), ]).then(function(){ // microperf: add totals, re-order into anew obj let btn = '', count = 0, newobj = {'all': {}} let iTime = 0, sTime = 0 // running totals let countInteger = 0 for (const k of Object.keys(oIntlLocalePerf).sort()) { newobj[k] = oIntlLocalePerf[k] let kTime = 0 // running sub total for (const j of Object.keys(oIntlLocalePerf[k]).sort()) { let value = oIntlLocalePerf[k][j] if ('constructors' == j) { count += value } else { if ('intl' == j) {iTime += value} else {sTime += value} kTime += value // sum time for this metric k if (Number.isInteger(value)) {countInteger++} // track integers if (isGecko && 16.67 == value.toFixed(2)) {countInteger++} // add RFP "integers" } } // add a subtotal if more than expected constructors|intl if (Object.keys(oIntlLocalePerf[k]).length > 2) {newobj[k]['total'] = kTime} } // we currently have 26 times // gecko noRFP + RFP = 26 | noRFP but reduceTimer + chrome = 0 to 5 if (countInteger < 13) { newobj.all = {'constructors': count, 'intl': iTime} if (sTime > 0) {newobj.all['string'] = sTime} newobj.all['total'] = iTime + sTime addDetail('intl', newobj, 'microperf') btn = addButton(99, 'intl','perf','btnc','microperf') //+' '+ countInteger } dom['intl_perf'].innerHTML = btn return resolve() }) }) }) }) const outputHeaders = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('headers')) {return resolve()} Promise.all([ get_nav_dnt('doNotTrack'), get_nav_gpc('globalPrivacyControl'), get_nav_connection('connection'), get_nav_online('onLine'), ]).then(function(){ return resolve() }) }) set_oIntlDates() set_oIntlDate() set_oIntlLocale() countJS(4) ================================================ FILE: js/screen.js ================================================ 'use strict'; /* SCREEN */ function return_lb(w,h) { // LB let wstep = 200, hstep = 200, bw = false, bh = false if (w < 501) {wstep = 50} else if (w < 1601) {wstep = 200} if (h < 501) {hstep = 50} else if (h < 1601) {hstep = 100} bw = Number.isInteger(w/wstep) bh = Number.isInteger(h/hstep) return (bw && bh) ? true : false } function return_nw(w,h) { // NW let wstep = w < 601 ? 50 : 200, hstep = h < 501 ? 50 : 100 let bw = false, bh = false if (w < 1401) {bw = Number.isInteger(w/wstep)} if (h < 901) {bh = Number.isInteger(h/hstep)} return (bw && bh) ? nw_green : nw_red } function get_scr_fs_measure() { // F11: triggered by resize events if in FS // fullscreenElement: called on android by outputUserFS if (gFS) {return} // don't run if already running gFS = true // set running state let delay = 25, max = 40, n = 1 // 40 x 25 = 1sec let w, h, firstW, firstH, lastW, lastH let isElementFS = document.fullscreen || document.webkitIsFullscreen || false let target = document.fullscreenElement //, range, data let output = isElementFS ? 'fullscreenElement' : 'fsSize' dom[output].innerHTML ='' // clear function measure() { if (isElementFS) { // use domrect but fallback to client if (isDomRect == -1) { w = document.fullscreenElement.clientWidth h = document.fullscreenElement.clientHeight } else if (isDomRect < 1) { let method = measureFn(target, output) w = method.width; h = method.height } } else { w = window.innerWidth; h = window.innerHeight } if (firstW == undefined) {firstW = w; firstH = h} // remember first values lastW = w; lastH = h return w +' x '+ h } // initial size let size = measure() let notation = rfp_red, strSteps = '' function check_size() { clearInterval(checking) let len = oDiffs.length if (len > 0) { let lastValue = oDiffs[len-1] let lastSize = lastW +' x '+ lastH let stepsTaken = ((lastValue.split(':')[0]) * 1) let timeTaken = stepsTaken * delay let diff = '[diff: '+ (w - firstW) +' x '+ (h - firstH) +']' timeTaken = Math.ceil(timeTaken/50) * 50 // round up in 50s size = size + s1 +' &#9654 '+ sc + lastSize strSteps = s1 +' <b>[~'+ timeTaken +' ms]</b> '+ sc + diff } // notate if (return_lb(lastW, lastH)) {notation = rfp_green} dom[output].innerHTML = size + (isSmart ? notation : '') + strSteps if (isElementFS) {document.exitFullscreen()} // only android can be isElementFS gFS = false // reset } let current = size, oDiffs = [], nochange = 0 function build_sizes() { if (n >= max) { check_size() } else { // grab changes try { let newsize = measure() if (newsize !== current) { nochange = 0 oDiffs.push(n +':'+ newsize) current = newsize } else { nochange++ if (nochange > 25) {check_size()} // exit } } catch { check_size() } } n++ } let checking = setInterval(build_sizes, delay) } const get_scr_fullscreen = (METRIC) => new Promise(resolve => { let oRes = {} let cssvalue = getElementProp(1, '#cssDM', METRIC +'_display-mode_css') let isErrCss = cssvalue == zErr function get_display_mode(item) { // https://developer.mozilla.org/en-US/docs/Web/CSS/@media/display-mode const item2 = item +'_css' let data, isLies = false try { let aValid = ['browser','fullscreen','miniumal-ui','standalone'] if (!isGecko) {aValid.push('picture-in-picture','window-controls-overlay')} for (let i=0; i < aValid.length; i++) { if (window.matchMedia('(display-mode:'+ aValid[i] +')').matches) {data = aValid[i]; break} } if (runST) {data = undefined} else if (runSL) {data += '_fake'} let typeCheck = typeFn(data) if ('string' !== typeCheck) {throw zErrType + typeCheck} if (isSmart && !isErrCss && data !== cssvalue) { log_known(1, METRIC +'_'+ item, data); isLies = true } } catch(e) { log_error(1, METRIC +'_'+ item, e); data = zErr } addDisplay(1, item, data,'','', isLies) oRes[item] = isLies ? zLIE : data oRes[item2] = cssvalue // get FS measurments if in FS let isElementFS = document.fullscreen || document.webkitIsFullscreen || false if (!isElementFS && 'fullscreen' == data && isDesktop) { get_scr_fs_measure() } else { gFS = false // cancel run state } } // nonGecko boolean vs undefined: i.e a string of "undefined" will be an error // note: FF145+ 1989559 marked as deprecated let expectedType = isGecko ? 'boolean' : 'undefined' // fullScreen function get_fullScreen(item) { let data, isLies = false try { data = window.fullScreen if (runST) {data = undefined} let typeCheck = typeFn(data) if (expectedType !== typeCheck) {throw zErrType + typeCheck} if ('undefined' == typeCheck) {data += ''} // lies let boolCss = 'fullscreen' == cssvalue ? true : false if (runSL) {data = !boolCss} if (isSmart && !isErrCss && boolCss !== data) { log_known(1, METRIC +'_'+ item, data) isLies = true } } catch(e) { log_error(1, METRIC +'_'+ item, e); data = zErr } // can't use fullScreen because blink returns window.fullScreen as the element id='fullScreen' addDisplay(1, 'window'+ item, data,'','', isLies) oRes[item] = isLies ? zLIE : data } // full-screen-api.enabled function get_mozFullScreenEnabled(item) { let data try { data = document.mozFullScreenEnabled if (runST) {data = undefined} let typeCheck = typeFn(data) if (expectedType !== typeCheck) {throw zErrType + typeCheck} if ('undefined' == typeCheck) {data += ''} } catch(e) { log_error(1, METRIC +'_'+ item, e); data = zErr } addDisplay(1, item, data) oRes[item] = data } // in order so oRes keys = sorted get_display_mode('display-mode') get_fullScreen('fullScreen') get_mozFullScreenEnabled('mozFullScreenEnabled') addData(1, METRIC, oRes, mini(oRes)) return resolve(oRes) }) const get_scr_measure = () => new Promise(resolve => { Promise.all([ get_scr_mm('measure'), get_scr_viewport('sizes_viewport'), get_scr_fullscreen('fullscreen'), ]).then(function(res){ // get FS status let isDisplayFS = 'fullscreen' == res[2]['display-mode'] let oTmp = { screen: {height: {}, width: {}}, available: {height: {}, width: {}}, inner: {height: {}, width: {}}, outer: {height: {}, width: {}}, } // matchmedia oTmp.screen.height.media = res[0]['device-height'] oTmp.screen.width.media = res[0]['device-width'] oTmp.inner.height.media = res[0].height oTmp.inner.width.media = res[0].width // test: css/media value is up to 1px higher: we should only allow a lower value //oTmp.screen.width.media = res[0]['device-width'] + 1 // small viewport units // desktop: same as viewport element + clientrect but it ignores scollbars, so can // add information and is a more precise measurement of matchMedia // android: used to calculate dynamic toolbar let vpWidth = isViewportUnits.width, vpHeight = isViewportUnits.height oTmp.inner.width['svw'] = vpWidth.svw oTmp.inner.height['svh'] = vpHeight.svh if (isDesktop) { // add desktop viewport so it's summarized/notated oTmp['viewport'] = res[1] } else { // android "document" is an inner size not viewport || calculate get dynamic toolbar oTmp.inner.width['document'] = vpWidth.document oTmp.inner.height['document'] = vpHeight.document // dynamic toolbar: display only - let it NaN for I care let strToolbar = (vpWidth.lvw - vpWidth.svw) +' x '+ (vpHeight.lvh - vpHeight.svh) addDisplay(1, 'dynamic_toolbar', strToolbar) // large viewport units addDisplay(1, 'viewport_large', vpWidth.lvw +' x '+ vpHeight.lvh) // sizes_viewport metric (small is already under sizes_inner) let newobj = {'height': {'lvh': vpHeight.lvh}, 'width': {'lvw': vpWidth.lvw}} addData(1, 'sizes_viewport', newobj, mini(newobj)) } // screen/window // order matters: so property targets are correct let aList = ['iframe','doc'] // iframe first let oList = { // screens first screen: ['height','width'], available: ['availHeight','availWidth'], // then windows with inner last outer: ['outerHeight','outerWidth'], inner: ['innerHeight','innerWidth'], } // window.inner on android is dynamic and also redudnant with document + small viewport units if (!isDesktop) {delete oList.inner} let iTarget, target try {iTarget = dom.tzpIframe.contentWindow} catch {} try {target = iTarget.screen} catch {} // initial iframe target aList.forEach(function(name) { if ('iframe' !== name) {target = screen; name = 'screen'} // initial target post iframe for (const k of Object.keys(oList)) { if ('iframe' !== name) { if ('outer' == k) {target = window; name = 'window'} // switch non-iframe target } let aItems = oList[k] for (let i=0; i < aItems.length; i++) { let p = aItems[i], x, isSkip = false let axis = p.includes('idth') ? 'width' : 'height' try { if ('iframe' == name && 'inner' == k) {isSkip = true} // skip iframe inner if (!isSkip) { if ('iframe' == name && 'outer' == k) {target = iTarget.window} // switch iframe target x = target[p] if (runST) {x = undefined} /* cause one error if (name == 'screen' && k == 'screen' && axis == 'width') {x = undefined} // fail one screen //*/ /* change one value: a little moot once we compare to css for zLIEs etc if (name == 'window' && k == 'outer' && axis == 'width') {x = x - 30} // fail one outer //*/ let typeCheck = typeFn(x) if ('number' !== typeCheck) {throw zErrType + typeCheck} // only matchmedia can be non Integer if (!Number.isInteger(x)) {throw zErrInvalid + 'expected Integer: got '+ typeCheck} } } catch(e) { log_error(1, 'sizes_'+ k +'_'+ axis +'_'+ name, e) x = zErr } if (!isSkip) {oTmp[k][axis][name] = x} } } }) // css let cssList = [['#S',':before'],['#S',':after'],['#D',':before'],['#D',':after']] cssList.forEach(function(array) { let cssID = array[0], pseudo = array[1] let axis = ':before' == pseudo ? 'width' : 'height' let metric = '#S' == cssID ? 'screen' : 'inner' let value = getElementProp(1, cssID, 'sizes_'+ metric +'_'+ axis +'_css', pseudo) if (value !== zErr && '?' !== value) { let cType = typeFn(value) if ('number' !== cType) { log_error(1, 'sizes_'+ metric +'_'+ axis +'_css', zErrType + cType) value = zErr } else if (!Number.isInteger(value)) { // only matchmedia can be non integer log_error(1, 'sizes_'+ metric +'_'+ axis +'_css', zErrInvalid + 'expected Integer: got '+ cType) value = zErr } } if ('#S' == array[0]) {oTmp.screen[axis].css = value} else {oTmp.inner[axis].css = value} }) // sort into new obj, build default display let oData = {}, oDisplay = {}, oSummary = {} for (const k of Object.keys(oTmp)) { oData[k] = {} oSummary[k] = {} for (const j of Object.keys(oTmp[k])) { oData[k][j] = {} oSummary[k][j] = undefined for (const m of Object.keys(oTmp[k][j]).sort()) { let value = oTmp[k][j][m] oData[k][j][m] = value if ('width' == j && 'css' !== m) { if ('svw' == m) {oDisplay[k +'_viewport'] = value +' x '+ oTmp[k]['height']['svh'] } else {oDisplay[k +'_'+ m] = value +' x '+ oTmp[k]['height'][m]} } // any error to oSummary, but ignore out of range css if ('string' == typeof value) { if ('css' == m && '?' == value) {} else {oSummary[k][j] = zErr} } } } } // initial oDisplay['initial_inner'] = isInitial.width.inner + ' x ' + isInitial.height.inner oDisplay['initial_outer'] = isInitial.width.outer + ' x ' + isInitial.height.outer // notation let notation ='', initData = zNA, initHash ='' let innerw = oData.inner.width.window, innerh = oData.inner.height.window let screenw = oData.screen.width.screen, screenh = oData.screen.height.screen // controls: we want integers so we know what to match to let controlw = innerw, controlh = innerh if (isDesktop) { addDisplay(1, 'size_newwin','','', return_nw(innerw, innerh)) // newwin } else { /* on android height - window.inner height can differ due to dynamic toolbar, so we use doc - documentElement height = clientHeight because we want the window, not the entire page length and is thus always our preferred integer to match to width - documentElement width = domrect and not an integer - but TZP uses a fixed width <meta name="viewport" content="width=800"> so width should be constant - but window.inner width is also not affected by dynamic toolbar - so use window in case a phone's native res exceeds or we drop (or lower) the fixed width */ controlh = oData.inner.height.document // controlw is undefined, we need to grab it try { controlw = window.innerWidth if ('number' !== typeFn(controlw)) {controlw = zErr} else if (!Number.isInteger(controlw)) {control = zErr} } catch { controlw = zErr } // initial_sizes // ToDo: add notation initData = isInitial; initHash = mini(isInitial) } let isCompareValid = 'number' == typeFn(controlw) && 'number' == typeFn(controlh) // desktop: if in fullscreenElement mode, use the svh element to measure // we don't have a resize event in android if (isDesktop) { let isElementFS = document.fullscreen || document.webkitIsFullscreen || false if (isElementFS) { addDisplay(1, 'fullscreenElement', oData.inner.width.svw +' x '+ oData.inner.height.svh) } try {dom.btnFS.style.display = (isElementFS ? 'block' : 'none')} catch {} } // RFP/match let aRound = ['media','css','svh','svw','document','element','visualViewport'] for (const k of Object.keys(oData)) { let isSame = true // for each axis for (const j of Object.keys(oData[k])) { let tmpSet = new Set for (const n of Object.keys(oData[k][j])) { let value = oData[k][j][n] let original = value //console.log(k, j, n, value) // ignore css out of range let isIgnore = '?' == value && 'css' == n if (zErr == value || zLIE == value) { isSame = false // errors and lies = fail isIgnore = true // don't add errors or lies to our set if (zErr == value) {oSummary[k][j] = zErr} } /* android window.inner a non-match doesn't matter for notation: we don't notate RFP inner on android yet it does cause a "mixed" in summary which with a green RFP is misleading, so ignore Note: once we add lies logic to our data, handled just above, we also won't be letting a possible genuine mismatch thru by not checking it for sameness */ if (!isDesktop && 'inner' == k && 'window' == n) {isIgnore = true} let control if (!isIgnore) { // *vw/h can be non-integer in inner // media can be non-integer | css can be off by 1 | both only screen + inner metrics // document (android) width uses domrect // viewport (desktop) ucan be non-integer // match them to our inner or screen if within 1 if (aRound.includes(n)) { value = Math.floor(value) // to remove non-integers + ensure valid diffs are positive control = 'width' == j ? screenw : screenh // if these are invalid diff == NaN if ('inner' == k || 'viewport' == k) {control = 'width' == j ? controlw : controlh} // we floored so any valid diff must be 1 or 0 because we substract value from control if (1 == control - value) {value = control} // match control } //if ('viewport' == k) {console.log(k, j, n, '\norig', original, '\ncontrol', control, '\nfinal value for sameness', value)} tmpSet.add(value) } } let aSet = Array.from(tmpSet) // summary // if the array is empty, isSame should already be false // we already rounded + matched non-integers, there should only be one if (aSet.length !== 1) { if (undefined == oSummary[k][j]) {oSummary[k][j] = 'mixed'} isSame = false } else { if (undefined == oSummary[k][j]) {oSummary[k][j] = aSet[0]} } // notation // if all the same then does it match _based_ on inner if (isSame && isCompareValid) { // inner + viewport: does it match LBing: check once and pass both width + height if ('inner' == k) { if ('width' == j) {isSame = return_lb(controlw, controlh)} } else if ('viewport' == k) { // viewport RFP must match letterboxing AND inner if ('width' == j) { isSame = return_lb(oSummary.viewport.width, oSummary.viewport.height) if (isSame) { // it must also match inner if (oSummary.viewport.width !== controlw) {isSame = false} if (oSummary.viewport.height !== controlh) {isSame = false} } } } else { // we can refine these rules later per key/OS: currently does it == inner let match = 'width' == j ? controlw : controlh if (aSet[0] !== match) {isSame = false} } } } let notation = isSame ? rfp_green : rfp_red if ('inner' == k && !isDesktop) {notation = ''} addDisplay(1, 'sizes_'+ k, '','', notation) } //console.log('viewport', res[1]) //console.log('data', oData) //console.log('sum', oSummary) //console.log('display', oDisplay) // health lookups if (gRun) { let strInner = oTmp.inner.width.window +' x '+ oTmp.inner.height.window let initInner = isInitial.width.inner +' x '+ isInitial.height.inner let initOuter = isInitial.width.outer +' x '+ isInitial.height.outer let initMatch = initInner == initOuter ? initInner : 'inner: '+ initInner +' | outer: '+ initOuter sDetail[isScope].lookup['size_newwin'] = strInner sDetail[isScope].lookup['sizes_initial'] = initMatch } /* ToDo: update oData/oDisplay/oSummary with lies i.e detect them, change oData to zLIE, color them sData[SECT99] covers "Screen.width","Screen.height","Screen.availWidth","Screen.availHeight" and we have if css is valid and it's not "isSame" i.e it matches within 1 about the only one we really can't tell is outer */ // display only: taskbar/dock + chrome // on android there is no dock and we set a minimum width which means chrome is non-sensical // and can be negative: e.g. outer 427 - inner 500, also display space is at a premium let arW = oData.screen.width.screen, arH = oData.screen.height.screen, dockH = arH - oData.available.height.screen, dockW = arW - oData.available.width.screen, chromeW = oData.outer.width.window - oData.inner.width.window, chromeH = oData.outer.height.window - oData.inner.height.window, aspect = Math.trunc((arW/arH) * 1000)/1000 if (isDesktop) { // ToDo: check each platform e.g. mac behavior FS Element vs F11 // chrome // note: non-gecko does not resize non-inner (e.g. outer) when zooming, so chrome sizes can get ridiculous, display anyway notation = red_benign let isChromeZero = '00' == chromeW +''+chromeH if (isDisplayFS) { if (isChromeZero) {notation = green_benign} } else { if (!isChromeZero) {notation = green_benign} } sDetail[isScope].lookup['size_chrome'] = chromeW +' x '+ chromeH addDisplay(1, 'size_chrome', '[chrome: '+ chromeW +' x '+ chromeH +']','', notation) // docker notation = red_benign let isDockZero = '00' == dockW +''+dockH if (isDisplayFS) { if (isDockZero) {notation = green_benign} } else { if (!isDockZero) {notation = green_benign} } let dockStr = ('windows' == isOS ? 'taskbar' : ('mac' == isOS ? 'menu bar/dock' : 'panel')) if (isOS == undefined) {dockStr = 'taskbar/dock/panel'} sDetail[isScope].lookup['size_dock'] = dockW +' x '+ dockH addDisplay(1, 'size_dock', '['+ dockStr +': '+ dockW +' x '+ dockH +']','', notation) } // aspect ratio // AR is one measurement that would be a finite list: that if not known would mean tampering (or a new AR) // https://en.wikipedia.org/wiki/List_of_common_display_resolutions // we can likely easily cover 95%+ of non android ARs, but the last 5% is a massive long tail. The data itself // redundant: and RFP checks in future will be set sizes e.g. 2k,4k,8k and will have their own halth checks // for now just display the AR notation = '' /* notation = isDesktop ? red_benign : '' sDetail[isScope].lookup['screen_aspect_ratio'] = aspect if ('NaN' !== aspect) { // always get highest over lowest to reduce checks let ar = (arW < arH ? Math.trunc((arH/arW) * 1000)/1000 : aspect) let aGood = [1.6, 1.777, 1.778] if (aGood.includes(ar)) {notation = green_benign} } //*/ addDisplay(1, 'screen_aspect_ratio', '[aspect ratio: '+ aspect +']','', notation) // data for (const k of Object.keys(oData)) {addData(1, 'sizes_'+ k, oData[k], mini(oData[k]))} addData(1, 'sizes_initial', initData, initHash) // display for (const k of Object.keys(oSummary)) {oDisplay[k +'_summary'] = oSummary[k].width +' x '+ oSummary[k].height} for (const k of Object.keys(oDisplay)) {addDisplay(1, k, oDisplay[k])} return resolve() }) }) const get_scr_mm = (datatype) => new Promise(resolve => { const unable = 'unable to find upper bound' const oList = { measure: [ ['sizes_screen', 'device-width', 'device-width', 'max-device-width', 'px', 512, 0.01], ['sizes_screen', 'device-height', 'device-height', 'max-device-height', 'px', 512, 0.01], ['sizes_inner', 'width', 'width', 'max-width', 'px', 512, 0.01], ['sizes_inner', 'height', 'height', 'max-height', 'px', 512, 0.01], ], pixels: [ ['pixels', '-moz-device-pixel-ratio', '-moz-device-pixel-ratio', 'max--moz-device-pixel-ratio', '', 4, 0.0000001], ['pixels', '-webkit-device-pixel-ratio', '-webkit-device-pixel-ratio', '-webkit-max-device-pixel-ratio', '', 4, 0.01], ['pixels', 'dpcm', 'resolution', 'max-resolution', 'dpcm', 1e-5, 0.0000001], ['pixels', 'dpi', 'resolution', 'max-resolution', 'dpi', 1e-5, 0.0000001], ['pixels', 'dppx', 'resolution', 'max-resolution', 'dppx', 1e-5, 0.0000001], ] } const oPrefixes = { 'device-width': 'sizes_screen_width', 'device-height': 'sizes_screen_height', width: 'sizes_inner_width', height: 'sizes_inner_height', '-moz-device-pixel-ratio': 'pixels', '-webkit-device-pixel-ratio': 'pixels', dpcm: 'pixels', dpi: 'pixels', dppx: 'pixels', } let list = oList[datatype], maxCount = oList[datatype].length, count = 0, oData = {} function exit(id, value) { if (value == unable) { if (!isGecko && '-moz-device-pixel-ratio' == id) { value = zNA } else { let suffix = (id.includes('width') || id.includes('height')) ? 'media' : id let metric = oPrefixes[id] +'_'+ suffix log_error(1, metric, unable) value = zErr } } oData[id] = value count++ if (count == maxCount) { return resolve(oData) } } function runTest(callback){ list.forEach(function(k){ let group = k[0], metric = k[1], lower = k[2], upper = k[3], suffix = k[4], epsilon = k[5], precision = k[6] Promise.all([ callback(group, lower, upper, suffix, epsilon, precision), ]).then(function(result){ if (runST) { exit(metric, unable) } else { exit(metric, result[0]) } }).catch(function(err){ exit(metric, err) }) }) } function searchValue(tester, maxValue, precision){ let minValue = 0 let ceiling = Math.pow(2, 32) function stepUp(){ if (maxValue > ceiling || runST){ return Promise.reject(unable) } return tester(maxValue).then(function(testResult){ if (testResult === searchValue.isEqual){ return maxValue } else if (testResult === searchValue.isBigger){ minValue = maxValue maxValue *= 2 return stepUp() } else { return false } }) } function binarySearch() { if (maxValue - minValue < precision) { return tester(minValue).then(function(testResult) { if (testResult.isEqual) {return minValue } else { return tester(maxValue).then(function(testResult) { if (testResult.isEqual) {return maxValue } else { return Promise.resolve(minValue) // +' to '+ maxValue // just return min } }) } }) } else { let pivot = (minValue + maxValue) / 2 return tester(pivot).then(function(testResult) { if (testResult === searchValue.isEqual) {return pivot } else if (testResult === searchValue.isBigger) { minValue = pivot return binarySearch() } else { maxValue = pivot return binarySearch() } }) } } return stepUp().then(function(stepUpResult) { if (stepUpResult){return stepUpResult } else {return binarySearch()} }) } searchValue.isSmaller = -1 searchValue.isEqual = 0 searchValue.isBigger = 1 runTest(function(group, prefix, maxPrefix, suffix, maxValue, precision) { return searchValue(function(valueToTest) { try { if (runSE) {foo++} if (window.matchMedia('('+ prefix +': '+ valueToTest + suffix+')').matches){ return Promise.resolve(searchValue.isEqual) } else if (window.matchMedia('('+ maxPrefix +': '+ valueToTest + suffix+')').matches){ return Promise.resolve(searchValue.isSmaller) } else { return Promise.resolve(searchValue.isBigger) } } catch(e) { let metric = oPrefixes[prefix] +'_media' if ('pixels' == group) {metric = group +'_'+ ('resolution' == prefix ? suffix : prefix)} log_error(1, metric, e, isScope) return Promise.reject(zErr) } }, maxValue, precision) }) }) const get_scr_orientation = (METRIC) => new Promise(resolve => { // NOTE: a screen.orientation.addEventListener('change'.. event // does not detect css changes, but a resize event does, which // is the only one we use, so treat css as truthy let oData = {'device': {}, 'window': {}}, oDisplay = {} // matchmedia: sorted names let oTests = { 'device': {'-moz-device-orientation': '#cssOm', 'device-aspect-ratio': '#cssDAR'}, 'window': {'aspect-ratio': '#cssAR', 'orientation': '#cssO'} } let l = 'landscape', p = 'portrait', q = '(orientation: ', s = 'square', a = 'aspect-ratio' for (const type of Object.keys(oTests)) { for (const item of Object.keys(oTests[type])) { let value, isErr = false let cssitem = item +'_css', cssID = oTests[type][item] try { if ('-moz-device-orientation' == item) { if (window.matchMedia('(-moz-device-orientation:'+ l +')').matches) value = l if (window.matchMedia('(-moz-device-orientation:'+ p +')').matches) value = p } else if ('device-aspect-ratio' == item) { if (window.matchMedia('(device-'+ a +':1/1)').matches) value = s if (window.matchMedia('(min-device-'+ a +':10000/9999)').matches) value = l if (window.matchMedia('(max-device-'+ a +':9999/10000)').matches) value = p } else if ('aspect-ratio' == item) { if (window.matchMedia('('+ a +':1/1)').matches) value = s if (window.matchMedia('(min-'+ a +':10000/9999)').matches) value = l if (window.matchMedia('(max-'+ a +':9999/10000)').matches) value = p } else { if (window.matchMedia(q + p +')').matches) value = p if (window.matchMedia(q + l +')').matches) value = l } if (runST) {value = undefined} else if (runSL) {value += '_fake'} // can only be undefined (default) or a string (which we set) if (!isGecko && '-moz-device-orientation' == item) { if (value !== undefined) {throw zErrType + typeFn(value)} // undefined in nonGecko value += '' } else { if (value == undefined) {throw zErrType +'undefined'} // we expect values (in gecko) } } catch(e) { log_error(1, METRIC +'_'+ type +'_'+ item, e) value = zErr isErr = true } // css // check matchmedia matches css let cssvalue = getElementProp(1, cssID, METRIC +'_'+ cssitem) let isErrCss = cssvalue == zErr let isLies = (!isErr && !isErrCss && value !== cssvalue) oDisplay[METRIC +'_'+ item] = {'value': value +'', 'lies': isLies} if (isSmart && isLies) { log_known(1, METRIC +'_'+ type +'_'+ item, value) value = zLIE } oData[type][item] = value oData[type][cssitem] = cssvalue } } // device: try and get a valid css value let check = oData.device['-moz-device-orientation_css'] if (zErr == check) {check = oData.device['device-aspect-ratio_css']} if ('square' == check) {check = 'portrait'} // screen // 1325110: mozOrientation slated for deprecation let items = ['mozOrientation', 'orientation.angle', 'orientation.type'] let targets = ['screen','iframe'], iscreen try {iscreen = dom.tzpIframe.contentWindow.screen} catch {} targets.forEach(function(k) { let strIframe = 'iframe' == k ? '_iframe' : '' let target = 'screen' == k ? screen : iscreen items.forEach(function(item) { let value, expectedType = 'string', isAngle = 'orientation.angle' == item, isLies = false try { if ('mozOrientation' == item) { value = target.mozOrientation // gecko: undefined throws an error, 'undefined' returns the string (or a lie if isSmart) if (!isGecko) {expectedType = 'undefined'} } else if (isAngle) { value = target.orientation.angle; expectedType = 'number' } else {value = target.orientation.type } if (runST) {value = isAngle ? value +'' : true } else if (runSI && isAngle) {value = 45 } else if (runSL) {value = isAngle ? 90 : 'portrait-primary' } let typeCheck = typeFn(value) if (expectedType !== typeCheck) {throw zErrType + typeCheck} if (isAngle) { let aGood = [0, 90, 180, 270] if (!aGood.includes(Math.abs(value))) { throw zErrInvalid + 'expected 0, 90, 180 or 270: got '+ value } } // lies if (isSmart && zErr !== check) { // check mozOrientation + .type matches css // note: we can't check the angle, it could be anything - see Piero tablet tests if ('string' == expectedType && value.split('-')[0] !== check) { log_known(1, METRIC +'_device_'+ item + strIframe, value) isLies = true } } } catch(e) { log_error(1, METRIC +'_device_'+ item + strIframe, e) value = zErr } if ('mozOrientation' == item && undefined == value) {value += ''} // only nonGecko mozOrientation can be undefined, we already threw oDisplay[METRIC +'_'+ item + strIframe] = {'value': value, 'lies': isLies} oData['device'][item + strIframe] = isLies ? zLIE : value }) }) // sort device object let tmpObj = {} for (const k of Object.keys(oData.device).sort()) {tmpObj[k] = oData.device[k]} oData.device = tmpObj // https://searchfox.org/mozilla-central/source/testing/web-platform/tests/screen-orientation/orientation-reading.html // see expectedAnglesLandscape + expectedAnglesPortrait // display, data for (const k of Object.keys(oDisplay)) {addDisplay(1, k, oDisplay[k]['value'],'','', oDisplay[k]['lies'])} for (const k of Object.keys(oData)) { // objects are already sorted let data = oData[k] let hash = mini(data) addData(1, METRIC +'_'+ k, oData[k], hash) if ('device' == k) { // create a summary // type: note: we already type checked mozOrientation on all engines and threw let aTemp = [data['orientation.type'], data['orientation.type_iframe']] if (isGecko) {aTemp.push(data.mozOrientation, data.mozOrientation_iframe) } else { if ('undefined' !== data.mozOrientation) {aTemp.push(data.mozOrientation)} if ('undefined' !== data.mozOrientation_iframe) {aTemp.push(data.mozOrientation_iframe)} } aTemp = dedupeArray(aTemp) let summary = aTemp.length > 1 ? 'mixed': aTemp[0] // angle aTemp = [data['orientation.angle'], data['orientation.angle_iframe']] aTemp = dedupeArray(aTemp) summary += ' | ' + (aTemp.length > 1 ? 'mixed': aTemp[0]) // orientation // note: aspect ratio can be square since we return that from css rather than portrait aTemp = [data['-moz-device-orientation'], data['-moz-device-orientation_css']] aTemp = dedupeArray(aTemp) let strOrientation = (aTemp.length > 1 ? 'mixed': aTemp[0]) if (!isGecko && 'undefined' == strOrientation) {strOrientation = ''} // aspect-ratio if ('mixed' !== strOrientation) { aTemp = [data['device-aspect-ratio'], data['device-aspect-ratio_css']] aTemp = dedupeArray(aTemp) let strAspect = (aTemp.length > 1 ? 'mixed': aTemp[0]) if (strOrientation !== strAspect) {strOrientation += (strOrientation.length ? ' + ': '') + strAspect} } summary += ' | ' + strOrientation addDisplay(1, METRIC +'_'+ k +'_summary', summary) // notation: use our summary // FF132+: 1607032 + 1918202 | FF133+: 1922204 | backported to BB // RFP is always primary | on android the angle of 0 vs 90 is reversed // type | angle | orientation (css) + aspect ratio (css) let oGood = { 'true': [ // desktop 'landscape-primary | 0 | landscape', 'portrait-primary | 90 | portrait', 'portrait-primary | 90 | portrait + square' ], 'false': [ // android 'landscape-primary | 90 | landscape', 'portrait-primary | 0 | portrait', 'portrait-primary | 0 | portrait + square', ] } let notation = oGood[isDesktop].includes(summary) ? rfp_green : rfp_red addDisplay(1, METRIC +'_'+ k,'','', notation) } } return resolve() }) const get_scr_pixels = (METRIC) => new Promise(resolve => { function get_dpr() { // DPR window let value, display, item = 'devicePixelRatio' let targets = ['window','iframe'] targets.forEach(function(k) { value = undefined let strIframe = 'iframe' == k ? '_iframe' : '' try { let target = 'window' == k ? window : dom.tzpIframe.contentWindow.window value = target.devicePixelRatio if (runST) {value = NaN} // this will also trigger dpi_div as varDPI is not set let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} display = value varDPR = value } catch(e) { log_error(1, METRIC +'_'+ item + strIframe, e) display = zErr value = zErr } // FF127: 1554751 let notation = value == 2 ? rfp_green : rfp_red addDisplay(1, METRIC +'_'+ item + strIframe, display, '', notation) oData[item + strIframe] = value }) // DPR border: 477157: don't notate this for health value = undefined, display = undefined, item = 'devicePixelRatio_border' try { value = getComputedStyle(dom.tzpDPR).borderTopWidth // e.g. '1px' if (runST) {value = undefined} else if (runSI) {value = '123'} let originalvalue = value let typeCheck = typeFn(value) if ('string' !== typeCheck) {throw zErrType + typeCheck} if (value.slice(-2) !== 'px') {throw zErrInvalid + 'got '+ originalvalue} // missing px value = value.slice(0, -2) if (value.length > 0) {value = value * 1} if ('number' !== typeFn(value)) {throw zErrInvalid + 'got '+ originalvalue} // missing number if (value > 0) { value = 1/value display = value varDPR = value } else { throw zErrInvalid + 'got '+ (1/value) // negative/Infinity } } catch(e) { display = log_error(1, METRIC +'_'+ item, e) value = zErr } addDisplay(1, item, display) oData[item] = value return } // DPI CSS function get_dpi_css(item) { let value = getElementProp(1, '#P', METRIC +'_'+ item, ':before') let typeCheck = typeFn(value) // ignore errors (already caught) and of out of range (entirely possible?) if (value !== '?' && value !== zErr) { if ('number' !== typeCheck) { log_error(1, METRIC +'_'+ item, zErrType + typeCheck), value = zErr } } // why did I allow a ? for css // was: 192 == value || '?' == value ? rfp_green : rfp_red addDisplay(1, METRIC +'_'+ item,'','', (192 == value ? rfp_green : rfp_red)) // css notate oData[item] = value } // DPI DIV function get_dpi_div(item) { /* this FP value is redundant: it's essentially 96 * our DPR PoC (IIUIC) IIUIC: monitors always have a native resolution of 96 dpi - our div element should always have a height of 96 - regardless of zoom + system scaling + layout.css.devPixelsPerPx (which combined == devicePixelRatio) - but IDK about e.g. QLED/Quantum "dots" and other emerging standards // tested with zooming levels: it's always 96 (domrect, offset, client) - system scaling 100% | 125% - layout.css.devPixelsPerPx 1.1 (equivalent to 110% zoom) - system scaling 125% + layout.css.devPixelsPerPx 1.1 combined */ let display, value try { let target = dom.tzpDPI // domrect will not give us any greater precision AFAICT, but why not let targetValue = 0 == isDomRect ? target.getBoundingClientRect().height : target.offsetHeight // the final "dpi" value comes from multiplying by DPR (our poc leak one) value = targetValue * varDPR let typeCheck = typeFn(value) if ('number' !== typeCheck) {throw zErrType + typeCheck} display = value } catch(e) { display = log_error(1, METRIC +'_'+ item, e), value = zErr } addDisplay(1, item, display) oData[item] = value } // visualViewport scale function get_vv_scale(item) { let value, display try { value = visualViewport.scale if (runST) {value = undefined} let typeCheck = typeFn(value) display = value if ('number' !== typeof value) {throw zErrType + typeCheck} } catch(e) { display = log_error(1, METRIC +'_'+ item, e) value = zErr } addDisplay(1, item, display) oData[item] = value return } // run let varDPR, oData = {} Promise.all([ get_scr_mm('pixels') ]).then(function(results){ for (const k of Object.keys(results[0])) { // expected 100% zoom values let oMatch = { '-moz-device-pixel-ratio': 2, '-webkit-device-pixel-ratio': 2, 'dpcm': 75.59054999999998, 'dpi': 192.00000000000006, 'dppx': 2, } let value = results[0][k] oData[k] = value addDisplay(1, METRIC +'_'+ k, value,'', (value == oMatch[k] ? rfp_green : rfp_red)) } get_dpr() // sets varDPR used in dpi_div get_dpi_css('dpi_css') get_dpi_div('dpi_div') if (isDesktop) { // android: useless, not stable as it is affected by zoom get_vv_scale('visualViewport_scale') } let newobj = {} for (const k of Object.keys(oData).sort()) {newobj[k] = oData[k]} addData(1, METRIC, newobj, mini(newobj)) // pixel matches if (isSmart) { get_scr_pixels_match('pixels_match', oData) } return resolve() }) }) function get_scr_pixels_match(METRIC, oData) { if (!isSmart) {return} // media pixels vs window devicePixelRatio let isPixelMatch = true, oPixels = {}, oSummary = {'false': [], 'true': []}, controlPx, testPx // remove items we don't compare let aIgnore = ['devicePixelRatio_border','dpi_div','visualViewport_scale'] aIgnore.forEach(function(item){delete oData[item]}) try { // typecheck for (const k of Object.keys(oData).sort()) { let typeCheck = typeFn(oData[k]) if ('number' !== typeCheck) { // ignore out-of-range css if ('dpi_css' == k && '?' == oData[k]) {} else {throw zErrInvalid + k +' expected number: got '+ typeCheck} } } let dprValue = oData.devicePixelRatio, dprStr = 'devicePixelRatio' let oControls = { '-moz-device-pixel-ratio': [dprValue, dprStr], '-webkit-device-pixel-ratio': [dprValue, dprStr], //'devicePixelRatio': it's the control 'devicePixelRatio_iframe': [dprValue, dprStr +''], 'dpcm': [dprValue * 96 / 2.54, dprStr +' * 96 / 2.54'], 'dpi': [dprValue * 96, dprStr +' * 96'], 'dpi_css': [dprValue * 96, dprStr +' * 96'], 'dppx': [dprValue, dprStr], } let oLists = { '-moz-device-pixel-ratio': ['-moz-device-pixel-ratio','max--moz-device-pixel-ratio','min--moz-device-pixel-ratio'], '-webkit-device-pixel-ratio': ['-webkit-device-pixel-ratio','-webkit-max-device-pixel-ratio','-webkit-min-device-pixel-ratio'], 'dppx': ['max-resolution','min-resolution','resolution'], } // b3e9e3c6 200% zoom dpr 1 === 100% zoom drp 1 with RFP //console.log(mini(oData), oData) for (const k of Object.keys(oData).sort()) { oPixels[k] = {} if (undefined !== oControls[k]) { controlPx = oControls[k][0] oPixels[k].control = oControls[k] } if ('devicePixelRatio_iframe' == k) { testPx = oData[k] == controlPx oPixels[k]['match'] = testPx if (false === testPx) {isPixelMatch = false} oSummary[testPx].push(k) } else if ('dpcm' == k || 'dpi' == k) { let diff = Math.abs(oData[k] - controlPx) oPixels[k].diff = diff let testPx = diff < 0.0001 oPixels[k].match = testPx if (false === testPx) {isPixelMatch = false} oSummary[testPx].push(k) } else if ('dpi_css' == k) { // ignore out-of-range dpi_css if ('?' !== oData[k]) { testPx = oData[k] == Math.floor(controlPx) // css is min-resolution oPixels[k]['match'] = testPx if (false === testPx) {isPixelMatch = false} oSummary[testPx].push(k) } } else if (oLists[k] !== undefined) { // ToDo: is max actually needed, so we need min? let unit = 'dppx' == k ? 'dppx' : '' oLists[k].forEach(function(item){ testPx = window.matchMedia('('+ item +':'+ controlPx + unit +')').matches oPixels[k]['match '+item] = testPx if (false === testPx) {isPixelMatch = false} oSummary[testPx].push(k +'_'+ item) }) } oPixels[k].value = oData[k] } // make the notification clickable sDetail[isScope][METRIC] = oPixels let btncolor = isPixelMatch ? 'good' : 'bad' let btnsymbol = isPixelMatch ? tick : cross addDisplay(1, METRIC,'','', addButton(btncolor, METRIC, "<span class='health'>"+ btnsymbol +"</span> RFP pixels")) } catch(e) { sDetail[isScope][METRIC] = e+'' addDisplay(1, METRIC,'','', sbx+' RFP pixels]'+sc) } } const get_scr_position_screen = (METRIC) => new Promise(resolve => { // left/top = 0 depends on secondary monitor | availLeft/availTop = 0 depends on dock/taskbar let tmpObj = {}, aList = ['availLeft','availTop','left','top'] // nonGecko: number vs undefined: i.e a string of "undefined" will be an error let aNonGecko = ['left','top'] let targets = ['screen','iframe'], iscreen, display = [] try {iscreen = dom.tzpIframe.contentWindow.screen} catch {} targets.forEach(function(k) { let strIframe = 'iframe' == k ? '_iframe' : '' let target = 'screen' == k ? screen : iscreen, x aList.forEach(function(item){ try { x = target[item] if (runST) {x = 'undefined'} let typeCheck = typeFn(x), expectedType = 'number' if (!isGecko && aNonGecko.includes(item)) {expectedType = 'undefined'} if (expectedType !== typeCheck) {throw zErrType + typeCheck} if (undefined == x) {x += ''} } catch(e) { log_error(1, METRIC +'_'+ item + strIframe, e); x = zErr } tmpObj[item + strIframe] = x }) }) // sort object let oData = {}, isMixed = false, btn ='' for (const k of Object.keys(tmpObj).sort()) {oData[k] = tmpObj[k]} //console.log(oData, mini(oData)) let hash = mini(oData) let notation = '4963ac89' == hash ? rfp_green : rfp_red addData(1, METRIC, oData, hash) aList.forEach(function(item){ let isMatch = oData[item] == oData[item +'_iframe'] if (!isMatch) {isMixed = true} display.push(isMatch ? oData[item] : 'mixed') }) if (isMixed) {btn = addButton(1, METRIC)} addDisplay(1, METRIC, display.join(', '), btn, notation) return resolve() }) const get_scr_position_window = (METRIC) => new Promise(resolve => { // FS = all 0 except sometimes mozInnerScreenY | maximized can include negatives for screenX/Y let oData = {}, aList = ['mozInnerScreenX','mozInnerScreenY','screenX','screenY'] // nonGecko: number vs undefined: i.e a string of "undefined" will be an error let aNonGecko = ['mozInnerScreenX','mozInnerScreenY'] let display = [], x aList.forEach(function(k){ try { x = window[k] if (runST) {x = 'undefined'} let typeCheck = typeFn(x), expectedType = 'number' if (!isGecko && aNonGecko.includes(k)) {expectedType = 'undefined'} if (expectedType !== typeCheck) {throw zErrType + typeCheck} if (undefined == x) {x += ''} } catch(e) { log_error(1, METRIC +'_'+ k, e); x = zErr } oData[k] = x; display.push(x) }) let hash = mini(oData) let notation = '66a7ee25' == hash ? rfp_green : rfp_red addDisplay(1, METRIC, display.join(', '), '', notation) addData(1, METRIC, oData, hash) return resolve() }) function get_scr_viewport_units() { // https://developer.mozilla.org/en-US/docs/Web/CSS/length // large, dynamic, small unit support: FF101, Safari 15.4, blink 108 // desktop + android use small in inner section // android uses large as a standalone let aList = isDesktop ? ['S'] : ['L','S'] let data = {'height': {}, 'width': {}} aList.forEach(function(k) { let METRIC = 'L' == k ? 'sizes_viewport' : 'sizes_inner' let target try {target = dom['tzp'+ k +'V']} catch {} let prefix = k.toLowerCase() + 'v' for (const p of Object.keys(data)) { //aItems.forEach(function(p) { let name = prefix + p.slice(0,1) try { let x if (isDomRect == -1) { x = p == 'width' ? target.offsetWidth : target.offsetHeight } else { let method = measureFn(target, METRIC +'_'+ prefix) if (undefined !== method.error) {throw method.errorstring} x = 'width' == p ? method.width : method.height //type check if (runST) {x = p == 'width' ? undefined : '' } let typeCheck = typeFn(x) if ('number' !== typeCheck) {throw zErrType + typeCheck} data[p][name] = x } } catch(e) { log_error(1, METRIC +'_'+ p + '_' + prefix + p.slice(0,1), e) data[p][name] = zErr } } }) return data } const get_scr_viewport = (METRIC) => new Promise(resolve => { // get viewport units isViewportUnits = get_scr_viewport_units() let oData = {height: {}, width: {}} const id= 'vp-element', aMETRIC = 'sizes_inner' function get_viewport(type) { let w, h, method, target let metric = isDesktop ? aMETRIC : METRIC try { if ('element' == type) { target = document.createElement('div') target.setAttribute('id', id) target.style.cssText = 'position:fixed;top:0;left:0;bottom:0;right:0;' document.documentElement.insertBefore(target,document.documentElement.firstChild) if (isDomRect == -1) { w = target.offsetWidth h = target.offsetHeight } else { method = measureFn(target, METRIC +'_'+ type) if (undefined !== method.error) {throw method.errorstring} w = method.width h = method.height } } else if ('document' == type) { // using document.documentElement + domrect = the full web content dimensions: we can // use domrect width as we know that is fixed | height we use clientHeight as this reports inner target = document.documentElement h = target.clientHeight if (isDomRect == -1) { w = target.clientWidth } else { method = measureFn(target, METRIC +'_'+ type) if (undefined !== method.error) {throw method.errorstring} w = method.width } } else { w = window.visualViewport.width h = window.visualViewport.height } if (runST) {w = NaN, h = undefined} let wType = typeFn(w), hType = typeFn(h) if ('number' !== wType) { log_error(1, metric +'_width_'+ type, zErrType + wType) w = zErr } if ('number' !== hType) { log_error(1, metric +'_height_'+ type, zErrType + hType) h = zErr } } catch(e) { h = zErr; w = zErr if (isDesktop) { log_error(1, metric +'_'+ type, e) } else { log_error(1, metric +'_width_'+ type, e) log_error(1, metric +'_height_'+ type, e) } } oData.height[type] = h //+ 100 oData.width[type] = w //+ 200 // android only calls document and uses it in inner section // we can just store this in isViewportUnits if (!isDesktop) { isViewportUnits.height['document'] = h isViewportUnits.width['document'] = w } } // ToDo: we could also use size observer / IntersectionObserverEntry // all get_viewport('document') // android: there is no viewport section: document becomes part of inner section. // element + visualViewport are redundant with a TZP clean load (new tab etc) and // can be or are unstable with dynamic urlbar/toolbar and pinch to zoom/reruns combos etc if (isDesktop) { get_viewport('element') get_viewport('visualViewport') removeElementFn(id) } // resolve return resolve(oData) // return the data for use in the parent function }) /* AGENT */ const get_agent = (METRIC, os = isOS) => new Promise(resolve => { let oReported = {'useragent': {}, 'useragentdata': {}} let oComplex = {}, oData = {}, countFail = 0, countSuccess = 0 /* windows: - FF116+ 1841425: windows hardcoded to 10.0 (patched 117 but 115 was last version for < win10) mac: - FF116+ 1841215: mac hardcoded to 10.15 (patched 117 but 115 was last release for < 10.15) android: - FF122+ 1865766: hardcod to 10.0 - partially backed out - FF123+ 1861847: hardcod oscpu/platform to 'Linux armv81' - FF126+ [pending: they shipped an intervention instead] 1860417: Linux added to appVersion + ua_os linux: - FF123+ 1861847: hardcode oscpu/platform to "Linux x86_64" (backed out? on hold? these are RFP's values anyway) - FF127+ 1873273: report non-x86_64 CPUs (including 32-bit x86) as "x86_64" */ /* - FF132+ 1711835 SPOOFED_PLATFORM dropped, is now hardcoded for all */ // RFP notation: nsRFPService.h let oRFP = { android: { appVersion: '5.0 (Android 10)', oscpu: 'Linux armv81', platform: 'Linux armv81', ua_os: 'Android 10; Mobile' }, linux: { appVersion: '5.0 (X11)', oscpu: 'Linux x86_64', platform: 'Linux x86_64', ua_os: 'X11; Linux x86_64' }, mac: { appVersion: '5.0 (Macintosh)', oscpu: 'Intel Mac OS X 10.15', platform: 'MacIntel', ua_os: 'Macintosh; Intel Mac OS X 10.15' }, windows: { appVersion: '5.0 (Windows)', oscpu: 'Windows NT 10.0; Win64; x64', platform: 'Win32', ua_os: 'Windows NT 10.0; Win64; x64' } } if (os !== undefined) { for (const k of Object.keys(oRFP)) { // important: only add next version to array if we are open ended ('+') let uaVer = isVer, isDroid = 'android' == k, nxtVer = uaVer + 1 // userAgent let uaRFP = 'Mozilla/5.0 (' + oRFP[k].ua_os +'; rv:', uaNext = uaRFP // base uaRFP += uaVer +'.0) Gecko/' + (isDroid ? uaVer +'.0' : '20100101') +' Firefox/'+ uaVer +'.0' oRFP[k].userAgent = [uaRFP] // next userAgent if ('+' == isVerExtra) { uaNext += nxtVer +'.0) Gecko/'+ (isDroid ? nxtVer +'.0' : '20100101') +' Firefox/'+ nxtVer +'.0' oRFP[k].userAgent.push(uaNext) } // desktop mode: 1727775 if (isDroid) { uaRFP = 'Mozilla/5.0 (' + oRFP.linux.ua_os +'; rv:', uaNext = uaRFP // base uaRFP += uaVer +'.0) Gecko/20100101 Firefox/'+ uaVer +'.0' oRFP[k].userAgent.push(uaRFP) if ('+' == isVerExtra) { uaNext += nxtVer +'.0) Gecko/20100101 Firefox/'+ nxtVer +'.0' oRFP[k].userAgent.push(uaNext) } } } } //console.log(oRFP) let list = { // static appCodeName: ['Mozilla', true], appName: ['Netscape', [1]], product: ['Gecko', null], buildID: ['20181001000000', 1], productSub: ['20100101', 1/0], vendor: ['empty string', {1:1}], vendorSub: ['empty string'], // more complex appVersion: ['skip', []], platform: ['skip', {}], oscpu: ['skip', NaN], userAgent: ['skip'], } for (const p of Object.keys(list).sort()) { oData[p] = ''; oReported[METRIC][p] = '' // preset ordered objects let expected = list[p][0], sim = list[p][1] let isErr = false, str ='' try { str = navigator[p] if (runST) {str = sim} else if (runSL) { // note: proxy lies are is only used on complex addProxyLie('Navigator.'+ p) // static we only check against expected: tampering is already recorded in // prototype lies and there is no need to record untrustworthy if it's expected if ('skip' !== expected) { str = ('FAKE '+ str).trim() } } let typeCheck = typeFn(str, true), expectedType = 'string' if (!isGecko) { // type check will throw an error for a string "undefined" if ('buildID' == p || 'oscpu' == p) {expectedType = 'undefined'} } if (expectedType !== typeCheck) {throw zErrType + typeFn(str)} if ('' == str) {str = 'empty string'} } catch(e) { isErr = true str = log_error(2, METRIC +'_'+ p, e) } if ('skip' !== expected) { outputStatic(p, str+'', expected, isErr) } else { oComplex[p] = [str+'', isErr] } } function outputStatic(property, reported, expected, isErr) { oReported[METRIC][property] = (isErr ? zErr : reported) //let isLies = isProxyLie('Navigator.'+ property) // prototypeLies doesn't pick everything up all the time: instead use expected // and because non-expected is lies // still notate slent fails so our count makes sense let isLies = reported !== expected let notation = (isErr || isLies) ? rfp_red : '' addDisplay(2, METRIC +'_'+ property, reported, '', notation, (isErr ? false : isLies)) let fpvalue = isErr ? zErr : (isSmart && isLies ? zLIE : reported) if (zLIE == fpvalue) {log_known(2, METRIC +'_'+ property, reported)} oData[property] = fpvalue } for (const k of Object.keys(oComplex)) { let reported = oComplex[k][0], isErr = oComplex[k][1] oReported[METRIC][k] = (isErr ? zErr : reported) let isLies = isProxyLie('Navigator.'+ k) if (!isLies) { let aFlags = [] // prototypeLies doesn't pick everything up all the time: add some basic checks // note: may be valid, e.g. a fork uses a custom values if ('userAgent' == k | 'appVersion' == k) { // userAgent: e.g. Chameleon, Chrome Mask // appVersion: User-Agent Switcher aFlags = [' like','Chrome','WebKit','KHTML','Apple','Safari'] for (let i=0; i < aFlags.length; i++) { if (reported.includes(aFlags[i])) {isLies = true; break} } } if (!isLies) { if ('userAgent' == k) { // check version: all platforms contain '; rv:' + version + '.0)' aFlags = [isVer] if ('+' == isVerExtra) {aFlags.push(isVer + 1)} let isVerCheck = false aFlags.forEach(function(item) { if (reported.includes('; rv:'+ item +'.0)')) {isVerCheck = true} }) if (!isVerCheck) {isLies = true} } else if ('appVersion' == k) { // User-Agent Switcher if ('windows' == os) {isLies = reported !== '5.0 (Windows)' } } else if ('platform' == k) { // User-Agent Switcher if ('windows' == os) {isLies = reported !== 'Win32' } } else if ('oscpu' == k) { // User-Agent Switcher if ('windows' == os) {isLies = !reported.includes('Windows NT 10.0') } } } } let notation = isLies ? rfp_red : '' // in case os is undefined if (os !== undefined) { let rfpvalue = oRFP[os][k], isMatch = false isMatch = (k == 'userAgent' ? rfpvalue.includes(reported) : rfpvalue === reported) notation = isMatch ? rfp_green : rfp_red // notate good desktopmode if (k == 'userAgent' && isMatch && !isDesktop) { if (reported.includes('Linux')) {notation = desktopmode_green} } // catch non-errors and non-lies health failures if (notation.includes(cross)) {countFail++} else if (notation.includes(tick)) {countSuccess++} } addDisplay(2, METRIC +'_'+ k, reported, '', notation, (isErr ? false : isLies)) // record value in oData let fpvalue = isErr ? zErr : (isSmart && isLies ? zLIE : reported) if (zLIE == fpvalue) {log_known(2, METRIC +'_'+ k, reported)} oData[k] = fpvalue } // add lookup addDetail('agent_reported', oReported) // add reported for matching/compares: e.g. iframes // add metric for (const k of Object.keys(oData)) {if (zLIE == oData[k] || zErr == oData[k]) {countFail++}} // count failures let strCount = (0 == countFail ? sg : sb) +'['+ countSuccess +'/'+ (countFail + countSuccess) +']'+ sc let agentnotation = (0 == countFail ? silent_rfp_green : silent_rfp_red) addBoth(2, METRIC, mini(oData), addButton(2, METRIC), agentnotation + strCount, oData) return resolve() }) const get_agent_data = (METRIC, os = isOS, isMain = true) => new Promise(resolve => { function exit(hash, data ='', btn ='') { if (isMain) { sDetail[isScope]['agent_reported'][METRIC] = ('object' == typeof data ? data : hash) addBoth(2, METRIC, hash, btn,'', data) } return resolve(data) } try { let k = navigator.userAgentData if (runSE) {foo++} else if (runST) {k = 1} else if (runSI) {k = {}} let typeCheck = typeFn(k, true) if ('undefined' == typeCheck) { exit(typeCheck) } else { // type check if ('object' !== typeCheck) {throw zErrType + typeCheck} let expected = '[object NavigatorUAData]' if (expected !== k+'') {throw zErrInvalid +'expected '+ expected +' got '+ k+''} // https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues navigator.userAgentData.getHighEntropyValues([ 'architecture','bitness','brands','formFactors','fullVersionList','mobile', 'model','platform','platformVersion','uaFullVersion','wow64' ]).then(res => { //let data = res // new object: merge versions + check for mismatches // e.g. brands, fullVersionList, uaFullVersion // keep order: e.g. opera vs chrome differs in order of array items // only blink so no smarts, for now just add the object exit(mini(res), res, addButton(2, METRIC)) }).catch(function(err){ exit(err, zErrLog) }) } } catch(e) { exit(e, zErrLog) } }) function get_agent_workers() { if (gRun && sectionIgnore.includes('agent')) {return} // control let list = ['appCodeName','appName','appVersion','platform','product','userAgent'] let oCtrl = {}, r list.forEach(function(prop) { try { r = navigator[prop] if ('string' !== typeof r) {throw zErr} if ('' ==r) {r = 'empty string'} } catch(e) { r = zErr } oCtrl[prop] = r }) let control = mini(oCtrl) // web let scope0 = 'worker', metric0 = 'agent_'+ scope0, target0 = dom[metric0], test0 ='' if (isFile) { target0.innerHTML = zSKIP } else { try { let workernav = new Worker('js/'+ scope0 +'_agent.js') target0.innerHTML = zF workernav.addEventListener('message', function(e) { //console.log(scope0, e.data) test0 = mini(e.data) target0.innerHTML = test0 + (test0 == control ? match_green : match_red) workernav.terminate }, false) workernav.postMessage('') } catch(e) { target0.innerHTML = log_error(2, metric0, e, scope0) } } // shared let scope1 = 'worker_shared', metric1 = 'agent_'+ scope1, target1 = dom[metric1], test1 ='' try { let sharednav = new SharedWorker('js/'+ scope1 +'_agent.js') target1.innerHTML = zF sharednav.port.addEventListener('message', function(e) { //console.log('scope1', e.data) test1 = mini(e.data) target1.innerHTML = test1 + (test1 == control ? match_green : match_red) sharednav.port.close() }, false) sharednav.port.start() sharednav.port.postMessage('') } catch(e) { target1.innerHTML = log_error(2, metric1, e, scope1) } // service let scope2 = 'worker_service', metric2 = 'agent_'+ scope2, target2 = dom[metric2], test2 ='' target2.innerHTML = zF // assume failure try { // register navigator.serviceWorker.register('js/'+ scope2 +'_agent.js').then(function(swr) { let sw if (swr.installing) {sw = swr.installing} else if (swr.waiting) {sw = swr.waiting} else if (swr.active) {sw = swr.active} sw.addEventListener('statechange', function(e) { if (e.target.state == 'activated') { sw.postMessage('') } }) if (sw) { // listen let channel = new BroadcastChannel('sw-agent') channel.addEventListener('message', event => { //console.log('agent service', event.data.msg) test2 = mini(event.data.msg) target2.innerHTML = test2 + (test2 == control ? match_green : match_red) // unregister & close swr.unregister().then(function(boolean) {}) channel.close() }) } else { target2.innerHTML = zF +' ['+ sw +']' } }, function(e) { target2.innerHTML = log_error(2, metric2, e, scope2) }) } catch(e) { target2.innerHTML = log_error(2, metric2, e, scope2) } } /* OUTPUT */ const outputFD = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('feature')) {return resolve()} let METRIC = 'infinity_architecture', value, data ='' try { const f = new Float32Array([Infinity - Infinity]) value = new Uint8Array(f.buffer)[3] } catch(e) { value = e; data = zErrLog } addBoth(3, METRIC, value,'','', data) // arch: FF110+ pref removed: error means 32bit let str = '64bit'; data = 64 if (isArch !== true) { if ('RangeError: invalid array length' == isArch) { str = '32bit'; data = 32 } else { str = isArch; data = zErr // blink we set zNA if expected error, so propigate that if ('blink' == isEngine && zNA == isArch) {data = zNA} } } addBoth(3, 'browser_architecture', str,'','', data) if (!isGecko) { let aList = ['logo','wordmark','version'] if (undefined == isOS) {aList.push('os')} aList.forEach(function(item) {addBoth(3, item, zNA)}) aList = ['tzpWordmark','tzpResource'] aList.forEach(function(item) {addDisplay(3, item, zNA)}) // browser addBoth(3, 'browser', isBrave ? 'Brave' : isEngine+'') // os if (undefined !== isOS) {addBoth(3, 'os', isOS)} return resolve() } // logo let wType, hType, w, h, isLogo, isLogoData ='', isWordmark, isWordData ='' try { w = dom.tzpAbout.width, h = dom.tzpAbout.height if (runST) {w += '', h = null} wType = typeFn(w), hType = typeFn(h) if ('number' !== wType || 'number' !== hType) {throw zErrType + wType +' x '+ hType} isLogo = w +' x '+ h } catch(e) { isLogo = e; isLogoData = zErrShort } // about-wordmark.svg // we were using width/height but in beta/dev 148 (may have started earlier) offscreen would // produce different and unstable results (see below) so instead we will use naturalW/H // ToDo: if I can work ouot how to make w/h stable and different this may help expose other methods // to differentiate channels and PB mode try { let isHidden, isOffscreen // hidden w = dom.tzpBrandHidden.naturalWidth, h = dom.tzpBrandHidden.naturalHeight if (runST) {w = true, h += ''} wType = typeFn(w), hType = typeFn(h) if ('number' !== wType || 'number' !== hType) {throw zErrType + wType +' x '+ hType} isHidden = w +' x '+ h // offscreen /* stable is one line "Firefox Browser" 336 x 48 - stable and matches nightly is two lines "Firefox | Nightly" 300 x 109 - stable and matches dev is two lines "Firefox | Developer" hidden 300 x 109 offscreen 628 x 227 <-- 1st time in a session and wtf ALWAYS THIS in PB windows offscreen 615 x 223 <-- thereafter beta is one line: "Firefox" hidden 300 x 67 offscreen 628 x 140 <-- 1st time in a session and wtf ALWAYS THIS in PB windows offscreen 615 x 137 <-- thereafter but offscreen should be 300 x 67 note: 1 1sec + pause before testing on gLoad solves stability but keeps the nonm-match but I don't think this adds anything entropy wise */ isOffscreen = dom.tzpBrand.naturalWidth +' x '+ dom.tzpBrand.naturalHeight if (isHidden !== isOffscreen) {isHidden += ', ' + isOffscreen} isWordmark = isHidden } catch(e) { isWordmark = e; isWordData = zErrShort } // set isMB: legacy: older 128's still need detection if (gLoad && !isBB && isDesktop) { if (128 == isVer && isWordmark + isLogo == '400 x 32300 x 236') { isMB = true isBB = true } } // browser let notation = isBB ? bb_red : '' addBoth(3, 'browser', (isMB ? 'Mullvad Browser' : (isTB ? 'Tor Browser' : 'Firefox'))) addBoth(3, 'logo', isLogo,'', (isBB && '24 x 24' == isLogo ? bb_green : notation), isLogoData) addBoth(3, 'wordmark', isWordmark,'', (isBB && '0 x 0' == isWordmark ? bb_green : notation), isWordData) // eval METRIC = 'eval.toString' try { let len = eval.toString().length if (runST) {len = 43} if (len !== 37) {throw zErrInvalid + 'expected 37: got '+ len} } catch(e) { log_error(3, METRIC, e) } // os, version addBoth(3, 'os', (isOS == undefined ? (isOSErr !== undefined ? isOSErr : zErr) : isOS)) addBoth(3, 'version', (isVerExtra !== '' ? isVer + isVerExtra : isVer)) // set metricsPrefix if (isGecko && isSmart) { metricsPrefix = (isMB ? 'MB' : (isTB ? 'TB': 'FF')) + isVer + isVerExtra +'-'+ (isOS !== undefined ? isOS : 'unknown') +'-' } return resolve() }) const outputScreen = (isResize = false) => new Promise(resolve => { if (gRun && sectionIgnore.includes('screen')) {return resolve()} Promise.all([ get_scr_position_screen('position_screen'), get_scr_position_window('position_window'), get_scr_pixels('pixels'), get_scr_orientation('orientation'), get_scr_measure(), ]).then(function(){ // add listeners once if (gLoad && isDesktop) { window.addEventListener('resize', function(){outputSection(1, true)}) } return resolve() }) }) const outputAgent = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('agent')) {return resolve()} Promise.all([ // keep order: useragent creates agent_reported lookup, and the others add to it get_agent('useragent'), // get_agent_data('useragentdata'), ]).then(function(){ // make agent_reported same structure as section let newobj = {}, data = sDetail[isScope].agent_reported for (const k of Object.keys(data).sort()) { if ('object' == typeof data[k]) {newobj[k] = {'hash': mini(data[k]), 'metrics': data[k]}} else {newobj[k] = data[k]} } sDetail[isScope].agent_reported = newobj return resolve() }) }) countJS(1) ================================================ FILE: js/storage.js ================================================ 'use strict'; /* Web SQL Database API / openDatabase don't test for this: it was implemented only in blink and safari - https://developer.chrome.com/blog/deprecating-web-sql - blink: API default disabled 119 (oct 2023) removed 124 (april 2024) - safari: removed in 2019 */ /* ToDo: leverage expires to get real date/time (substract our constant - e.g 2 days) cookieStore.getAll().then(cookies=>console.log(cookies)) some engines return more information e.g. chrome includes expires: 1765940558000 */ function lookup_cookie(name) { try { name += '=' let decodedCookie = decodeURIComponent(document.cookie) let ca = decodedCookie.split(';') for (let i=0; i < ca.length; i++) { let c = ca[i] while (c.charAt(0) == ' ') {c = c.substring(1)} if (c.indexOf(name) == 0) {return c.substring(name.length, c.length)} } } catch {} return '' } const lookup_cookiestore = async function(rndStr, k) { try { let cookie = await cookieStore.get(rndStr + k) return cookie.value } catch(e) { return '' } } const lookup_permission = (item) => new Promise(resolve => { try { navigator.permissions.query({name: item}).then(function(r) { return resolve(r.state) }).catch(e => { return resolve() }) } catch(e) { return resolve() } }) function lookup_storage_bucket(type, bytes, granted = false) { const GiB = 1073741824 // test //bytes = Math.floor(((32/5) * GiB)) // = 6.4 exact //bytes = Math.floor(((32/5) * GiB)) -1 // = 6-7 range //bytes = Math.floor(((32/5) * GiB)) +1 // = 6-7 range //bytes = 5368709119 // 5GiB minus 1 byte = 4-5 range let value = (bytes/GiB) // in GiBs let isExact = Number.isInteger(value) if (!isExact) { // catch obvious floating points: i.e a part byte difference :) // e.g. 32GiB * 20% (gecko's %) = 6.4GiB = but we get 6.3999999994412065 // 6.4 * GiB = 6871947673.6 (gecko floors) let upper = (Math.ceil(value *10)/10) // e.g. 6.4 let diff = (upper * GiB) - bytes //console.log('bytes', bytes,'\nvalue', value,'\nupper', upper,'\ndiff', diff) if (diff < 1) { isExact = true value = upper } } if (!isExact) { // still not exact, floor it value = Math.floor(bytes/(GiB) * 10)/10 } if ('quota' == type && !isExact) { // bucketize quota more // if persistent-storage is granted // if gecko and under 10GB // if blink which doesn't protect this | webkit IDK it seems to provide precise values let isBucket = (isGecko && value < 10 || !isGecko || granted) if (isBucket) { if (value < 10) { // more precision value = Math.floor(value) value = value +'-'+ (value + 1) } else { // round to next 100, return range value = Math.ceil(value/100) * 100 value = value-100 +'-'+ value } } } // webkit private window returns 1048576000 bytes = 1000MB if ('webkit' == isEngine && 1048576000 == bytes) {value = '1000 MB'} else {value += ' GiB'} // blink incognito returns 1819735497 bytes = some reduced calculation? return value } const get_caches = (METRIC) => new Promise(resolve => { let t0 = nowFn() // PB mode: DOMException: The operation is insecure. // FF122: 1864684: dom.cache.privatebrowsing.enabled // also see 1742344 / 1714354 // type check first // e.g. insecure parent on http://www.raymondhill.net/ublock/pageloadspeed.html let typeCheck = typeFn(window.self.caches, true) try { if ('object' !== typeCheck) { throw zErrType + typeCheck } else { Promise.all([ window.self.caches.keys() ]).then(function(){ exit(zE) }).catch(function(e){ exit(log_error(6, METRIC, e)) }) } } catch(err) { exit(log_error(6, METRIC, err)) } function exit(str) { addBoth(6, METRIC, str,'','', (str = zE ? str : zErr)) log_perf(6, METRIC, t0) return resolve() } }) function get_cookies(METRIC, rndStr) { let value try { let test = navigator.cookieEnabled if (runST) {test = undefined} let typeCheck = typeFn(test) if ('boolean' !== typeCheck) {throw zErrType + typeCheck} value = test ? zE : zD } catch(e) { log_error(6, METRIC, e); value = zErr } let aTests = ['_session','_persistent'] aTests.forEach(function(k){ try { let expires ='' if ('_persistent' == k) { let d = new Date() d.setTime(d.getTime() + 172800000) // 2 days expires = '; expires='+ d.toUTCString() } document.cookie = rndStr + k +'='+ rndStr +'; SameSite=Strict' + expires value += ' | '+ (lookup_cookie(rndStr + k) == rndStr ? zS : zF) } catch(e) { log_error(6, METRIC + k, e); value += ' | '+ zErr } }) // don't use cookie in element names == adblockers might block display addDisplay(6, 'ctest', value) addData(6, METRIC, value) return } const get_cookiestore = (METRIC, rndStr) => new Promise(resolve => { // https://developer.mozilla.org/en-US/docs/Web/API/CookieStore function exit() { // don't use cookie in element names == adblockers might block display addDisplay(6, 'cstest', value) addData(6, METRIC, value) return resolve() } let value, obj = window[METRIC] try { value = 'object' == typeFn(obj, true) ? zE : zD } catch(e) { log_error(6, METRIC, e); value = zErr } if (isFile) { value += ' | '+ zSKIP + ' | '+ zSKIP exit() } else { // use a different suffix than cookies let aTests = ['_session_store','_persistent_store'] aTests.forEach(function(k){ try { let options = {name: rndStr + k, value: rndStr} if ('_persistent_store' == k) { options['expires'] = Date.now() + 172800000 // 2 days } cookieStore.set(options) Promise.all([ lookup_cookiestore(rndStr, k), ]).then(function(res){ value += ' | ' + (res[0] == rndStr ? zS : zF) if ('_persistent_store' == k) {exit()} }) } catch(e) { // slice "_store": consistent style to match cookies // redundant to use "cookieStore_session_store" log_error(6, METRIC + k.slice(0,-6), e); value += ' | '+ zErr if ('_persistent_store' == k) {exit()} } }) } }) function get_filesystem(METRIC) { let display = isFileSystem, notation ='' if (isFileSystem === zErr) { display = log_error(6, METRIC, isFileSystemError) } // PBmode: SecurityError: Security error when calling GetDirectory if (isBB) { notation = ('SecurityError: Security error when calling GetDirectory' == isFileSystemError ? bb_green : bb_red) } else { // FF111: 1811001: dom.fs.enabled = true if (isFileSystem == zD) {notation = default_red} } addBoth(6, METRIC, display,'', notation, isFileSystem) return } function get_idb(METRIC) { let value = zE try { let test = window[METRIC] if (runST) {test = []} let typeCheck = typeFn(test, true) if ('undefined' == typeCheck) {value = typeCheck } else if ('object' !== typeCheck) {throw zErrType +typeCheck} } catch(e) { log_error(6, METRIC, e); value = zErr } addBoth(6, METRIC, value) return } function get_storage(METRIC, rndStr) { // dom.storage.enabled let value, type = ('localStorage' == METRIC ? 'local' : 'session') let obj try { obj = window[type +'Storage'] value = 'object' == typeFn(obj, true) ? zE : zD } catch(e) { log_error(6, METRIC, e); value = zErr } try { if (runSE) {foo++} obj.setItem(rndStr +'_'+ type, rndStr) value += ' | '+ (obj.getItem(rndStr +'_'+ type) == rndStr ? zS : zF) } catch(e) { log_error(6, METRIC +'_test', e); value += ' | '+ zErr } addBoth(6, METRIC, value) return } const get_storage_quota = (METRIC) => new Promise(resolve => { let isLies = false, notation = rfp_red let isAuto = false Promise.all([ lookup_permission('persistent-storage') ]).then(function(res){ if ('granted' == res[0] || 'denied' == res[0]) {isAuto = true} // no prompt try { let test = 'storage_quota' == METRIC ? navigator.storage : navigator.webkitTemporaryStorage if (undefined == test) { exit('undefined') } else { navigator.storage.estimate().then(estimate => { let bytes = estimate.quota if (runST) {bytes = undefined} else if (runSL) {addProxyLie('StorageManager.estimate')} let typeCheck = typeFn(bytes) if ('number' !== typeCheck && !Number.isInteger(bytes)) {throw zErrType + typeCheck} let value = lookup_storage_bucket('quota', bytes, isAuto) let display = value +' ['+ bytes +' bytes]' if (isProxyLie('StorageManager.estimate')) {isLies = true} // 1781277 RFP can only be exactly 10GB or 50GB if (10737418240 == bytes || 53687091200 == bytes) {notation = rfp_green} sDetail[isScope].lookup[METRIC] = display exit(display, value) }).catch(function(e){ exit(log_error(6, METRIC, e), zErr) }) } } catch(e) { exit(log_error(6, METRIC, e), zErr) } }) function exit(display, value) { addBoth(6, METRIC, display,'', notation, value, isLies) // silent run manager to force granted quota when run if (isAuto) { Promise.all([outputUserStorageManager()]).then(function(){return resolve()}) } else { return resolve() } } }) function get_workers(METRIC) { // these are kinda redundant because we have them in window properties metric, and in future // we will type check and use their scopes in the overall fingerptint: until then ... let aList = ['ServiceWorker','SharedWorker','Worker'] let data = {}, aStr = [] aList.forEach(function(k){ let value = zE try { let test = window[k] if (runST) {test = false} let typeCheck = typeFn(test) if ('undefined' == typeCheck) {value = typeCheck } else if ('function' !== typeCheck) {throw zErrType + typeCheck} } catch(e) { log_error(6, METRIC +'_'+ k, e); value = zErr } data[k] = value aStr.push(value) }) addDisplay(6, METRIC, aStr.join(' | ')) addData(6, METRIC, data, mini(data)) return } const test_idb = (log = false) => new Promise(resolve => { let t0 = nowFn(), rndStr = rnd_string() const METRIC = 'indexedDB_test' function exit(value) { dom[METRIC] = value if (log) {log_perf(SECTNF, METRIC, t0,'', value)} return resolve() } try { let openIDB = indexedDB.open(rndStr +'_idb') // create openIDB.onupgradeneeded = function(event){ let dbObject = event.target.result let dbStore = dbObject.createObjectStore(rndStr, {keyPath:'id'}) } openIDB.onsuccess = function(event) { let dbObject = event.target.result // start let dbTx = dbObject.transaction(rndStr, 'readwrite') let dbStore = dbTx.objectStore(rndStr) // add dbStore.put({id: rndStr, value: rndStr}) // query let getStr = dbStore.get(rndStr) getStr.onsuccess = function() { exit(getStr.result.value == rndStr ? zS : zF) } // close dbTx.oncomplete = function() {dbObject.close()} } openIDB.onerror = function(event) {exit(zF)} } catch { exit(zErr) } }) const test_worker = (log = false) => new Promise(resolve => { let t0 = nowFn() let METRIC = 'worker_test' function exit(value) { dom[METRIC].innerHTML = value if (log) {log_perf(SECTNF, METRIC, t0,'', value)} return resolve() } if ('undefined' == typeof Worker) { exit('undefined') } else { try { const workerScript = `self.postMessage('eek')` const workerBlob = new Blob([workerScript], {type: 'application/javascript'}) const workerURL = URL.createObjectURL(workerBlob) const worker = new Worker(workerURL) worker.onmessage = function(e) {worker.terminate; exit(zS)} // receive worker.onerror = function(e) {exit(zErr)} // error worker.onterminate = function() {URL.revokeObjectURL(workerURL)} // cleanup } catch { exit(zErr) } } }) const test_worker_service = (log = false) => new Promise(resolve => { let t0 = nowFn() const METRIC = 'worker_service_test' function exit(value) { dom[METRIC] = value if (log) {log_perf(SECTNF, METRIC, t0,'', value)} } if ('undefined' == typeof ServiceWorker) { exit('undefined') } else { try { navigator.serviceWorker.register('js/storage_service_worker.js').then((registration) => { exit(zS) registration.unregister().then(function(boolean) {}) }) .catch((error) => { exit(zErr) }) } catch {exit(zErr)} } }) const test_worker_shared = (log = false) => new Promise(resolve => { let t0 = nowFn() const METRIC = 'worker_shared_test' function exit(value) { dom[METRIC] = value if (log) {log_perf(SECTNF, METRIC, t0,'', value)} return resolve() } if ('undefined' == typeof SharedWorker) { exit('undefined') } else { try { let shared = new SharedWorker('js/storage_shared_worker.js') let rndStr2 = rnd_string() shared.port.addEventListener('message', function(e) { let value = ('TZP-'+ rndStr2 === e.data) ? zS : zF shared.port.close() exit(value) }, false) shared.onerror = function (err) {exit(zErr)} shared.port.start() shared.port.postMessage(rndStr2) } catch { exit(zErr) } } }) const test_worker_shared_new = (log = false) => new Promise(resolve => { let t0 = nowFn() let METRIC = 'worker_shared_test' function exit(value) { dom[METRIC].innerHTML = value +' TEST' if (log) {log_perf(SECTNF, METRIC, t0,'', value)} return resolve() } if ('undefined' == typeof SharedWorker) { exit('undefined') } else { try { const workerScript = 'var ports = []; onconnect = function(e) {let port = e.ports[0]; ports.push(port); ' + 'port.start(); port.onmessage = function(e) {port.postMessage("eek")}' const workerBlob = new Blob([workerScript], {type: 'application/javascript'}) const workerURL = URL.createObjectURL(workerBlob) const shared = new SharedWorker(workerURL) shared.port.postMessage('eek') // ping shared.onmessage = function(e) {port.close(); shared.terminate; exit(zS)} // receive shared.onerror = function(e) { console.log('onerror', e, e.message) exit(zErr) } // error shared.onterminate = function() {URL.revokeObjectURL(workerURL)} // cleanup } catch(e) { console.log('trycatch', e) exit(zErr) } } }) const outputStorage = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('storage')) {return resolve()} let rndStr = rnd_string() Promise.all([ get_idb('indexedDB'), get_workers('workers'), get_cookies('cookies', rndStr), get_storage('localStorage', rndStr), get_storage('sessionStorage', rndStr), get_cookiestore('cookieStore', rndStr), get_caches('caches'), get_filesystem('filesystem'), get_storage_quota('storage_quota'), ]).then(function(){ return resolve() }) }) countJS(6) ================================================ FILE: js/storage_service_worker.js ================================================ 'use strict'; ================================================ FILE: js/storage_shared_worker.js ================================================ 'use strict'; // shared var ports = [] onconnect = function(e) { let port = e.ports[0] ports.push(port) port.start() port.onmessage = function(e) {port.postMessage("TZP-"+e.data)} } ================================================ FILE: js/user.js ================================================ 'use strict'; /* USER */ function exitUserFS() { try {document.exitFullscreen()} catch {} } const outputUserAgentOpen = (METRIC) => new Promise(resolve => { let list = ['appCodeName','appName','appVersion','buildID','oscpu', 'platform','product','productSub','userAgent','vendor','vendorSub'] let data = {'useragent': {}, 'useragentdata': {}}, r let newWin = window.open() let newNavigator = newWin.navigator function exit(value) { newWin.close() data['useragentdata'] = value // make agent_reported same structure as section let newobj = {} for (const k of Object.keys(data).sort()) { if ('object' == typeof data[k]) {newobj[k] = {'hash': mini(data[k]), 'metrics': data[k]}} else {newobj[k] = data[k]} } data = newobj // hash let hash = mini(data) const ctrlHash = mini(sDetail.document.agent_reported) // output if (hash == ctrlHash) { hash += match_green } else { addDetail(METRIC, data) hash += addButton(2, METRIC) + match_red } addDisplay(2, METRIC, hash) return resolve() } // useragent list.forEach(function(p) { try { r = newNavigator[p] let typeCheck = typeFn(r, true), expectedType = 'string' if (!isGecko) { // type check will throw an error for a string "undefined" if ('buildID' == p || 'oscpu' == p) {expectedType = 'undefined'} } if (expectedType !== typeCheck) {throw zErr} if ('' == r) {r = 'empty string'} } catch(e) { r = e } data['useragent'][p] = r+'' }) // useragentdata try { let k = navigator.userAgentData let typeCheck = typeFn(k, true) if ('undefined' == typeCheck) { exit(typeCheck) } else { if ('object' !== typeCheck) {throw zErr} if ('[object NavigatorUAData]' !== k+'') {throw zErr} navigator.userAgentData.getHighEntropyValues([ 'architecture','bitness','brands','formFactors','fullVersionList','mobile', 'model','platform','platformVersion','uaFullVersion','wow64' ]).then(res => { exit(res) }).catch(function(err){ exit(zErr) }) } } catch(e) { exit(zErr) } }) const outputUserAudio = (METRIC) => new Promise(resolve => { // oscillator const get_oscillator = (metric) => new Promise(resolve => { let btn ='' function exit(value, data) { if (undefined !== data) { sDetail[isScope][metric] = data btn = addButton(11, metric) } addDisplay(11, metric, value, btn) return resolve([metric, value]) } try { if (runSE) {foo++} let results = [], audioCtx = new window.AudioContext let oscillator = audioCtx.createOscillator(), analyser = audioCtx.createAnalyser(), gain = audioCtx.createGain(), scriptProcessor = audioCtx.createScriptProcessor(4096, 1, 1) gain.gain.value = 0 oscillator.type = 'triangle' oscillator.connect(analyser) analyser.connect(scriptProcessor) scriptProcessor.connect(gain) gain.connect(audioCtx.destination) scriptProcessor.onaudioprocess = function(bins) { try { bins = new Float32Array(analyser.frequencyBinCount) analyser.getFloatFrequencyData(bins) // JSShelter errors here if ('object' !== typeFn(bins)) {throw zErrType +'Float32Array: '+ typeFn(bins)} for (let i=0; i < bins.length; i++) {results.push(bins[i])} analyser.disconnect() scriptProcessor.disconnect() gain.disconnect() // output if (runSL) {results = []} let typeCheck = typeFn(results[0]) if ('number' !== typeCheck) {throw zErrType + typeCheck} let hash = mini(results) exit(hash, results) } catch(e) { exit(log_error(11, metric, e), e+'') } } oscillator.start(0) } catch(e) { exit(log_error(11, metric, e), e+'') } }) // hybrid const get_oscillator_compressor = (metric) => new Promise(resolve => { let btn ='' function exit(value, data) { if (undefined !== data) { sDetail[isScope][metric] = data btn = addButton(11, metric) } addDisplay(11, metric, value, btn) return resolve([metric, value]) } try { let results = [] let audioCtx = new window.AudioContext, oscillator = audioCtx.createOscillator(), analyser = audioCtx.createAnalyser(), gain = audioCtx.createGain(), scriptProcessor = audioCtx.createScriptProcessor(4096, 1, 1) // compressor let compressor = audioCtx.createDynamicsCompressor() compressor.threshold && (compressor.threshold.value = -50) compressor.knee && (compressor.knee.value = 40) compressor.ratio && (compressor.ratio.value = 12) compressor.reduction && (compressor.reduction.value = -20) compressor.attack && (compressor.attack.value = 0) compressor.release && (compressor.release.value = .25) gain.gain.value = 0 // 0 volume oscillator.type = 'triangle' // wave oscillator.connect(compressor) compressor.connect(analyser) analyser.connect(scriptProcessor) scriptProcessor.connect(gain) gain.connect(audioCtx.destination) scriptProcessor.onaudioprocess = function(bins) { try { bins = new Float32Array(analyser.frequencyBinCount) analyser.getFloatFrequencyData(bins) // JSShelter errors here if ('object' !== typeFn(bins)) {throw zErrType +'Float32Array: '+ typeFn(bins)} for (let i=0; i < bins.length; i++) {results.push(bins[i])} analyser.disconnect() scriptProcessor.disconnect() gain.disconnect() // check if (runSE) {foo++} else if (runSL) {results = []} let typeCheck = typeFn(results[0]) if ('number' !== typeCheck) {throw zErrType + typeCheck} let hash = mini(results) exit(hash, results) } catch(e) { exit(log_error(11, metric, e), e+'') // user test: reflect error entropy } } oscillator.start(0) } catch(e) { exit(log_error(11, metric, e), e+'') // user test: reflect error entropy } }) // run let section = {} function run() { let notation = rfp_red try { let tStart = nowFn() let test = new window.AudioContext Promise.all([ get_oscillator(METRIC +'_oscillator'), get_oscillator_compressor(METRIC +'_oscillator_compressor'), ]).then(function(results){ section[results[0][0]] = results[0][1] // oscillator section[results[1][0]] = results[1][1] // oscillator_compressor let obj = {} for (const k of Object.keys(section).sort()) {obj[k.replace('audio_test_', '')] = section[k]} let hash = mini(obj) addDetail(METRIC, obj) if (true === isArch) { if ('e2bbb839' == hash) { // {"oscillator": "5b3956a9", "oscillator_compressor": "e08487bf"} notation = sgtick+'x86_64/amd_64]'+sc } else if ('011d0e6e' == hash) { // {"oscillator": "f263f055", "oscillator_compressor": "1f38e089"} notation = sgtick+'ARM64/aarch64]'+sc } } else if ('e9f98e24' == hash) { // {"oscillator": "e9f98e24", "oscillator_compressor": "bafe56d6"} notation = sgtick+'x86/i686/ARMv7]'+sc } addDisplay(11, METRIC, hash, addButton(0, METRIC, Object.keys(section).length +' metrics'), notation) if (isPerf) { sDataTemp['perf'].push([2, METRIC, performance.now() - tStart, performance.now()]) output_perf(METRIC) } return resolve() }) } catch(e) { addDisplay(11, METRIC, log_error(11,'audio2', e), '', notation) return resolve() } } // start Promise.all([ outputPrototypeLies(), ]).then(function(){ run() }) }) const outputUserFS = (METRIC) => new Promise(resolve => { gFS = false try { if (isDesktop) { // desktop: use documentElement // we can scroll, click, view everything // let the resize event trigger running the section // let get_scr_measure check for document.fullscreen and fill in the display // use svh because otherwise the height is the full document height document.documentElement.requestFullscreen() return resolve() } else { let element = dom.tzpFS Promise.all([ element.requestFullscreen() ]).then(function(){ get_scr_fs_measure() return resolve() }) } } catch(e) { addDisplay(1, METRIC, log_error(1, METRIC, e)) return resolve() } }) const outputUserNewWin = (METRIC) => new Promise(resolve => { let sizesi = [], // inner history sizeso = [], // outer history n = 1, // setInterval counter newWinLeak ='' // open // was: tests/newwin.html // use about:blank (same as forcing a delay with a non-existant website) let newWin = window.open('about:blank','width=9000,height=9000') //let newWin = window.open('tests/newwin.html','width=9000,height=9000') let iw = newWin.innerWidth, ih = newWin.innerHeight, ow = newWin.outerWidth, oh = newWin.outerHeight sizesi.push(iw +' x '+ ih) sizeso.push(ow +' x '+ oh) // default output newWinLeak = iw +' x '+ ih +' [inner] '+ ow +' x '+ oh +' [outer]' function check_newwin() { let changesi = 0, changeso = 0 // detect changes let prev = sizesi[0] let strInner = s1 +'inner: '+ sc + iw +' x '+ ih for (let k=0; k < sizesi.length; k++) { if (sizesi[k] !== prev ) { changesi++; strInner += s1 +' &#9654 <b>['+ k +']</b> '+ sc + sizesi[k] } prev = sizesi[k] } prev = sizeso[0] let strOuter = s1 +'outer: '+ sc + ow +' x '+ oh for (let k=0; k < sizeso.length; k++) { if (sizeso[k] !== prev ) { changeso++; strOuter += s1 +' &#9654 <b>['+ k +']</b> '+ sc + sizeso[k] } prev = sizeso[k] } // one or two lines if (changesi > 0 || changeso > 0) { newWinLeak = strInner +'<br>'+ strOuter } // output addDisplay(1, METRIC, newWinLeak) return resolve() } function build_newwin() { // check n times as fast as we can/dare if (n == 150) { clearInterval(checking) check_newwin() } else { // grab metrics try { sizesi.push(newWin.innerWidth +' x '+ newWin.innerHeight) sizeso.push(newWin.outerWidth +' x '+ newWin.outerHeight) } catch { clearInterval(checking) // if not 'permission denied', eventually we always get // NS_ERROR_UNEXPECTED which we can ignore. Always output //console.log(e) //console.log(n, sizesi, sizeso) check_newwin() } } n++ } let checking = setInterval(build_newwin, 3) }) const outputUserPointer = (METRIC, event) => new Promise(resolve => { // ToDo: also look at radiusX/Y, screenX/Y, clientX/Y // https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/28535#note_2906361 if (window.PointerEvent === undefined) { addDisplay(7, METRIC, 'undefined') return resolve() } let oData = {'pointerdown': {}, 'pointerrawupdate': isPointerRawUpdate} let oList = { isPrimary: 'boolean', // RFP true pressure: 'number', // RFP: 0 if not active, 0.5 if active mozPressure: 'number', pointerType: 'string', // RFP mouse mozInputSource: 'number', // mouse = 1, pen = 2, touch = 5 tangentialPressure: 'number', // RFP 0 tiltX: 'number', // RFP 0 tiltY: 'number', // RFP 0 twist: 'number', // RFP 0 width: 'number', // RFP 1 height: 'number', // RFP 1 altitudeAngle: 'number', azimuthAngle: 'number', } if (!isGecko) { oList.mozPressure = 'undefined' oList.mozInputSource = 'undefined' } for (const k of Object.keys(oList).sort()) { let value = event[k], expected = oList[k], typeCheck = typeFn(value) if (typeCheck !== expected) { value = zErrType + typeCheck } if ('undefined' == typeCheck) {value += ''} oData['pointerdown'][k] = value } let hash = mini(oData), btn = addButton(7, METRIC) sDetail[isScope][METRIC] = oData addDisplay(7, METRIC, hash, btn) return resolve() }) const outputUserStorageManager = (isUserTest = false, METRIC = 'storage_manager') => new Promise(resolve => { // note: delay = 0 and !isUSerTest = silent run if permission granted on main TZP test let notation = rfp_red function exit(value) { addDisplay(6, METRIC, value,'', notation) return resolve() } try { if (undefined == navigator.storage) { exit('undefined') } else { navigator.storage.persist().then(function(persistent) { navigator.storage.estimate().then(estimate => { // we don't care about estimate.usage let bytes = estimate.quota // bytes let typeCheck = typeFn(bytes) if ('number' === typeCheck && Number.isInteger(bytes)) { let value = lookup_storage_bucket('manager', bytes) value += ' ['+ bytes +' bytes]' if (isProxyLie('StorageManager.estimate')) { value = log_known(6, METRIC, value) } else { // 1781277 RFP can only be exactly 10GiB or 50GiB if (10737418240 == bytes || 53687091200 == bytes) {notation = rfp_green} } exit(value) } else { throw zErrType + typeCheck } }).catch(function(e){exit(log_error(6, METRIC, e))}) }).catch(function(e){exit(log_error(6, METRIC, e))}) } } catch(e) {exit(log_error(6, METRIC, e))} }) const outputUserTimingAudio = (METRIC) => new Promise(resolve => { // contexttime: geckoview // TypeError: undefined (with and with and w/out RFP)) on first run sometimes (and sometimes subsequent runs) // seen in FF139 stable, 141 beta, 142 nightly let aList = ['contexttime','performancetime'], oTime = {}, audioCtx, source, rAF aList.forEach(function(k){ gData.timing[k] = [] oTime[k] = [] }) // collect function collectTimestamps() { const ts = audioCtx.getOutputTimestamp(); oTime.contexttime.push(ts.contextTime * 1000) oTime.performancetime.push(ts.performanceTime) rAF = requestAnimationFrame(collectTimestamps); // Reregister itself if (oTime.contexttime.length > 20) {stop()} } // record try { audioCtx = new AudioContext() source = new AudioBufferSourceNode(audioCtx); source.start(0); rAF = requestAnimationFrame(collectTimestamps) } catch(e) { addDisplay(17, METRIC, log_error(17, METRIC, e),'', rfp_red) return resolve() } // finish function stop() { source.stop(0) cancelAnimationFrame(rAF) aList.forEach(function(k){ let data = oTime[k] data = dedupeArray(data) // contextTime: if the first value (we deduped) is 0 then we need to drop it // otherwise the first diff causes an offset to our 60FPS timing as rAF catches up: e.g. // 0, 10, 26.6, 43.3, 76.6, 110, 143.3, 160, 176.6, 193.3, 210, 243.3 // 0, 10, 26.6, 43.3 // ^ should be 0, 16.6, 33.3: i.e the [0, 10, 26.6...] we drop the start point of 0 // after that everythng is in sync if ('contexttime' == k && 0 == data[0]) {data = data.slice(1)} gData.timing[k] = data }) Promise.all([ get_timing(METRIC) ]).then(function(){ return resolve() }) } }) function outputUser(x, event) { // manual tests: require user initiated, permissions, transient activity // do nothing if (isBlock || !gClick) {return} // if already in fullscreenElement, nothing to do // we already did it when entering and resize picks up changes if ('fullscreenElement' == x) { if (document.fullscreen || document.webkitIsFullscreen) { return } } sDataTemp.display.manual = {} // reset display data gClick = false // prevent other tests gRun = false // reset get_isPerf() // reset isScope = 'manual' // promise var promiseTest = async function(x) { if ('agent_open' == x) { return(outputUserAgentOpen(x))} if ('audio_test' == x) { return(outputUserAudio(x))} if ('fullscreenElement' == x) { return(outputUserFS(x))} if ('newwin' == x) { return(outputUserNewWin(x))} if ('pointer_event' == x) { return(outputUserPointer(x, event))} if ('storage_manager' == x) { return(outputUserStorageManager(true))} if ('timing_audio' == x) { return(outputUserTimingAudio(x))} } // ToDo: add an x option to run all (except FS) if ('all' == x) { } else { try {dom[x] = ''} catch {} // clear // clear additional try { let items = document.getElementsByClassName('u'+x) for (let i=0; i < items.length; i++) {items[i].innerHTML = '&nbsp'} } catch {} let noDelay = ['audio','newwin', 'timing_audio'] let delay = noDelay.includes(x) ? 0 : 170 setTimeout(function() { Promise.all([ promiseTest(x) ]).then(function(){ gClick = true let target = sDataTemp.display.manual for (const k of Object.keys(target)) {dom[k].innerHTML = target[k]} }) }, delay) } } countJS('user') ================================================ FILE: js/webgl.js ================================================ 'use strict'; /* modifed from https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2 */ function get_webgl() { /* ToDo: view-source:https://privacy-test-pages.glitch.me/privacy-protections/fingerprinting/helpers/tests.js MOAR stuff to be recorded here */ const WebGLConstants = [ 'ALIASED_LINE_WIDTH_RANGE', 'ALIASED_POINT_SIZE_RANGE', 'ALPHA_BITS', 'BLUE_BITS', 'DEPTH_BITS', 'GREEN_BITS', 'MAX_COMBINED_TEXTURE_IMAGE_UNITS', 'MAX_CUBE_MAP_TEXTURE_SIZE', 'MAX_FRAGMENT_UNIFORM_VECTORS', 'MAX_RENDERBUFFER_SIZE', 'MAX_TEXTURE_IMAGE_UNITS', 'MAX_TEXTURE_SIZE', 'MAX_VARYING_VECTORS', 'MAX_VERTEX_ATTRIBS', 'MAX_VERTEX_TEXTURE_IMAGE_UNITS', 'MAX_VERTEX_UNIFORM_VECTORS', 'MAX_VIEWPORT_DIMS', 'RED_BITS', 'RENDERER', 'SHADING_LANGUAGE_VERSION', 'STENCIL_BITS', 'VENDOR', 'VERSION' ] const WebGL2Constants = [ 'MAX_VARYING_COMPONENTS', 'MAX_VERTEX_UNIFORM_COMPONENTS', 'MAX_VERTEX_UNIFORM_BLOCKS', 'MAX_VERTEX_OUTPUT_COMPONENTS', 'MAX_PROGRAM_TEXEL_OFFSET', 'MAX_3D_TEXTURE_SIZE', 'MAX_ARRAY_TEXTURE_LAYERS', 'MAX_COLOR_ATTACHMENTS', 'MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS', 'MAX_COMBINED_UNIFORM_BLOCKS', 'MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS', 'MAX_DRAW_BUFFERS', 'MAX_ELEMENT_INDEX', 'MAX_FRAGMENT_INPUT_COMPONENTS', 'MAX_FRAGMENT_UNIFORM_COMPONENTS', 'MAX_FRAGMENT_UNIFORM_BLOCKS', 'MAX_SAMPLES', 'MAX_SERVER_WAIT_TIMEOUT', 'MAX_TEXTURE_LOD_BIAS', 'MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS', 'MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS', 'MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS', 'MAX_UNIFORM_BLOCK_SIZE', 'MAX_UNIFORM_BUFFER_BINDINGS', 'MIN_PROGRAM_TEXEL_OFFSET', 'UNIFORM_BUFFER_OFFSET_ALIGNMENT' ] const Categories = { data: [ //uniformBuffers 'MAX_UNIFORM_BUFFER_BINDINGS', 'MAX_UNIFORM_BLOCK_SIZE', 'UNIFORM_BUFFER_OFFSET_ALIGNMENT', 'MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS', 'MAX_COMBINED_UNIFORM_BLOCKS', 'MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS', //fragmentShader 'MAX_FRAGMENT_UNIFORM_VECTORS', 'MAX_TEXTURE_IMAGE_UNITS', 'MAX_FRAGMENT_INPUT_COMPONENTS', 'MAX_FRAGMENT_UNIFORM_COMPONENTS', 'MAX_FRAGMENT_UNIFORM_BLOCKS', 'FRAGMENT_SHADER_BEST_FLOAT_PRECISION', 'MIN_PROGRAM_TEXEL_OFFSET', 'MAX_PROGRAM_TEXEL_OFFSET', //frameBuffer 'MAX_DRAW_BUFFERS', 'MAX_COLOR_ATTACHMENTS', 'MAX_SAMPLES', 'RGBA_BITS', 'DEPTH_STENCIL_BITS', 'MAX_RENDERBUFFER_SIZE', 'MAX_VIEWPORT_DIMS', //rasterizer 'ALIASED_LINE_WIDTH_RANGE', 'ALIASED_POINT_SIZE_RANGE', //textures 'MAX_TEXTURE_SIZE', 'MAX_CUBE_MAP_TEXTURE_SIZE', 'MAX_COMBINED_TEXTURE_IMAGE_UNITS', 'MAX_TEXTURE_MAX_ANISOTROPY_EXT', 'MAX_3D_TEXTURE_SIZE', 'MAX_ARRAY_TEXTURE_LAYERS', 'MAX_TEXTURE_LOD_BIAS', //transformFeedback 'MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS', 'MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS', 'MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS', //vertexShader 'MAX_VARYING_VECTORS', 'MAX_VERTEX_ATTRIBS', 'MAX_VERTEX_TEXTURE_IMAGE_UNITS', 'MAX_VERTEX_UNIFORM_VECTORS', 'MAX_VERTEX_UNIFORM_COMPONENTS', 'MAX_VERTEX_UNIFORM_BLOCKS', 'MAX_VERTEX_OUTPUT_COMPONENTS', 'MAX_VARYING_COMPONENTS', 'VERTEX_SHADER_BEST_FLOAT_PRECISION', // was info 'ANTIALIAS', ], info: [ //'CONTEXT', //'DIRECT_3D', 'MAJOR_PERFORMANCE_CAVEAT', 'RENDERER', 'SHADING_LANGUAGE_VERSION', 'VENDOR', 'VERSION', 'UNMASKED_VENDOR_WEBGL', 'UNMASKED_RENDERER_WEBGL', ], } /* parameter helpers */ // https://developer.mozilla.org/en-US/docs/Web/API/EXT_texture_filter_anisotropic const getMaxAnisotropy = (context) => { try { const extension = ( context.getExtension('EXT_texture_filter_anisotropic') || context.getExtension('WEBKIT_EXT_texture_filter_anisotropic') || context.getExtension('MOZ_EXT_texture_filter_anisotropic') ) return context.getParameter(extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT) } catch (error) { console.error(error) return undefined } } // https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_draw_buffers const getMaxDrawBuffers = (context) => { try { const extension = ( context.getExtension('WEBGL_draw_buffers') || context.getExtension('WEBKIT_WEBGL_draw_buffers') || context.getExtension('MOZ_WEBGL_draw_buffers') ) return context.getParameter(extension.MAX_DRAW_BUFFERS_WEBGL) } catch (error) { return undefined } } // https://developer.mozilla.org/en-US/docs/Web/API/WebGLShaderPrecisionFormat/precision // https://developer.mozilla.org/en-US/docs/Web/API/WebGLShaderPrecisionFormat/rangeMax // https://developer.mozilla.org/en-US/docs/Web/API/WebGLShaderPrecisionFormat/rangeMin const getShaderData = (shader) => { const shaderData = {} try { for (const prop in shader) { const shaderPrecisionFormat = shader[prop] shaderData[prop] = { precision: shaderPrecisionFormat.precision, rangeMax: shaderPrecisionFormat.rangeMax, rangeMin: shaderPrecisionFormat.rangeMin } } return shaderData } catch (error) { return undefined } } // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getShaderPrecisionFormat const getShaderPrecisionFormat = (context, shaderType) => { const props = ['LOW_FLOAT', 'MEDIUM_FLOAT', 'HIGH_FLOAT'] const precisionFormat = {} try { props.forEach(prop => { precisionFormat[prop] = context.getShaderPrecisionFormat(context[shaderType], context[prop]) return }) return precisionFormat } catch (error) { return undefined } } // https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_debug_renderer_info const getUnmasked = (contextType, context, constant) => { try { const extension = context.getExtension('WEBGL_debug_renderer_info') const unmasked = context.getParameter(extension[constant]) return unmasked } catch (e) { log_error(10, contextType +'_'+ constant, e) return zErr } } /* get WebGLRenderingContext or WebGL2RenderingContext */ // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext function getWebGL(contextType) { const errors = [] let data = {} const isWebGL = /^(experimental-)?webgl$/ const isWebGL2 = /^(experimental-)?webgl2$/ const supportsWebGL = isWebGL.test(contextType) && 'WebGLRenderingContext' in window const supportsWebGL2 = isWebGL2.test(contextType) && 'WebGLRenderingContext' in window // detect support if (!supportsWebGL && !supportsWebGL2) { errors.push('not supported') return [data, errors] } // get canvas context let canvas let context let hasMajorPerformanceCaveat try { canvas = document.createElement('canvas') context = canvas.getContext(contextType, { failIfMajorPerformanceCaveat: true }) if (!context) { hasMajorPerformanceCaveat = true context = canvas.getContext(contextType) if (!context) { throw new Error(`context of type ${typeof context}`) } } } catch (e) { errors.push(['context', e+'']) // 'context blocked' return [data, errors] } // get supported extensions // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getSupportedExtensions // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Using_Extensions let webGLExtensions try { webGLExtensions = context.getSupportedExtensions() } catch (e) { errors.push(['extensions', e+'']) // 'extensions blocked' } // get parameters let parameters try { const VERTEX_SHADER = getShaderData(getShaderPrecisionFormat(context, 'VERTEX_SHADER')) const FRAGMENT_SHADER = getShaderData(getShaderPrecisionFormat(context, 'FRAGMENT_SHADER')) parameters = { ANTIALIAS: context.getContextAttributes().antialias, //CONTEXT: contextType, MAJOR_PERFORMANCE_CAVEAT: hasMajorPerformanceCaveat, MAX_TEXTURE_MAX_ANISOTROPY_EXT: getMaxAnisotropy(context), MAX_DRAW_BUFFERS_WEBGL: getMaxDrawBuffers(context), VERTEX_SHADER, VERTEX_SHADER_BEST_FLOAT_PRECISION: Object.values(VERTEX_SHADER.HIGH_FLOAT), FRAGMENT_SHADER, FRAGMENT_SHADER_BEST_FLOAT_PRECISION: Object.values(FRAGMENT_SHADER.HIGH_FLOAT), UNMASKED_VENDOR_WEBGL: getUnmasked(contextType, context, 'UNMASKED_VENDOR_WEBGL'), UNMASKED_RENDERER_WEBGL: getUnmasked(contextType, context, 'UNMASKED_RENDERER_WEBGL') } const glConstants = [...WebGLConstants, ...(supportsWebGL2 ? WebGL2Constants : [])] glConstants.forEach(key => { const result = context.getParameter(context[key]) const typedArray = result && ( result.constructor === Float32Array || result.constructor === Int32Array ) parameters[key] = typedArray ? [...result] : result }) parameters.RGBA_BITS = [ parameters.RED_BITS, parameters.GREEN_BITS, parameters.BLUE_BITS, parameters.ALPHA_BITS, ] parameters.DEPTH_STENCIL_BITS = [ parameters.DEPTH_BITS, parameters.STENCIL_BITS, ] // redundant //parameters.DIRECT_3D = /Direct3D|D3D(\d+)/.test(parameters.UNMASKED_RENDERER_WEBGL) } catch (e) { log_error(10, contextType, e) errors.push(['parameters', e+'']) // 'parameters blocked' } // Structure parameter data let components = {} if (parameters) { Object.keys(Categories).forEach((name) => { const componentData = Categories[name].reduce((acc, key) => { if (parameters[key] !== undefined) { acc[key] = parameters[key] } return acc }, {}) // compile if data exists if (Object.keys(componentData).length) { components[name] = componentData } }) } data = { ...components, webGLExtensions } return [data, errors] } Promise.all([ getWebGL('webgl'), getWebGL('webgl2'), getWebGL('experimental-webgl'), ]).then((response) => { //console.log(response) const [webGL, webGL2, experimentalWebGL] = response const [webGLData, webGLErrors] = webGL const [webGL2Data, webGL2Errors] = webGL2 const [experimentalWebGLData, experimentalWebGLErrors] = experimentalWebGL // NS click to play: not entropy: only guaranteed on FIRST session page load assuming the // exception hasn't been permanently saved. We already have entropy on safer vs standard // and NS, so a Safer with allowed webgl implies clickedToPlay //let isClickToPlay = !!document.querySelector('.__ns__pop2top [data-policy-type="webgl"]') //* if (!isFile) { console.debug('WebGL: ', mini(webGLData), webGLData) if (webGLErrors.length) {console.log('webGL Errors',webGLErrors)} console.debug('WebGL2: ', mini(webGL2Data), webGL2Data) if (webGL2Errors.length) {console.log('webGL2 Errors',webGL2Errors)} console.debug('Experimental: ', mini(experimentalWebGLData), experimentalWebGLData) if (experimentalWebGLErrors.length) {console.log('Experimental Errors',experimentalWebGLErrors)} } //*/ // do something with the erorrs... return }).catch(error => { console.error(error) return }) } const outputWebGL = () => new Promise(resolve => { if (gRun && sectionIgnore.includes('webgl')) {return resolve()} // ToDo: readPixels, webGPU Promise.all([ get_webgl(), ]).then(function(){ return resolve() }) }) countJS(10) ================================================ FILE: js/worker_agent.js ================================================ 'use strict'; addEventListener('message', function(e) { let list = ['appCodeName','appName','appVersion','platform','product','userAgent'] let data = {}, r list.forEach(function(p) { try { r = navigator[p] if ('string' !== typeof r) {throw 'error'} if ('' == r) {r = 'empty string'} } catch(e) { r = e } data[p] = r }) self.postMessage(data) }, false) ================================================ FILE: js/worker_service_agent.js ================================================ 'use strict'; addEventListener('message', function(e) { let list = ['appCodeName','appName','appVersion','platform','product','userAgent'] let data = {}, r list.forEach(function(p) { try { r = navigator[p] if ('string' !== typeof r) {throw 'error'} if ('' == r) {r = 'empty string'} } catch(e) { r = e } data[p] = r }) let channel = new BroadcastChannel('sw-ua') channel.postMessage({msg: data}) }, false) ================================================ FILE: js/worker_shared_agent.js ================================================ 'use strict'; var ports = [] onconnect = function(e) { let port = e.ports[0] ports.push(port) port.start() port.onmessage = function(e) { let list = ['appCodeName','appName','appVersion','platform','product','userAgent'] let data = {}, r list.forEach(function(p) { try { r = navigator[p] if ('string' !== typeof r) {throw "error"} if ('' == r) {r = 'empty string'} } catch(e) { r = e } data[p] = r }) port.postMessage(data) } } ================================================ FILE: tests/applang-xslterror.xml ================================================ <?xml version="1.0"?><?xml-stylesheet type="text/xsl" href=""?><a>a</a> ================================================ FILE: tests/applang.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>app language</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <link rel="stylesheet" href="chrome://global/locale/intl.css"> <link rel="preload" href="applang-invalid.png" as="image"> <link rel="preload" href="applang-image.png" as="image"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 780px} .appfixed { top: 0; left: 0; padding: 0px; position: fixed; display: inline; width:fit-content; height:fit-content; } .appbigger { font-size: 250px; } </style> </head> <body> <div class="appfixed normalized"><div class="appbigger skew" id="elementsdiv"></div></div> <!-- offscreen --> <div class="offscreen"> <div> <!-- applang-image.png can't be hidden otherwise "Scaled..." is not in the title --> <iframe id="InvalidImage" width="100" height="30" src="applang-invalid.png"></iframe> <iframe id="ScaledImage" width="100" height="30" src="applang-image.png"></iframe> <iframe id="tzpXSLT" width="40" height="30" src="applang-xslterror.xml"></iframe> <div id='tzpDirection'></div> <div><input id="enumber" type="number" min="6" step="2"></div> </div> </div> <div class="hidden"> <div> <input type="text" required id="widgettext"> <input type="checkbox" required id="widgetcheckbox"> <input type="date" id="widgetdatetime" value="2024-01-01" max="2023-12-31"> <input type="date" id="widgetdatetimeunder" value="2022-01-01" min="2023-12-31"> <input type="email" id="widgetemail" value="a"> <input type="file" required id="widgetfile"> <input type="number" required id="widgetnumber"> <input type="number" id="widgetmax" max="1974.3" value="2000"> <input type="number" id="widgetmin" min="8026.5" value="1"> <input type="number" id="widgetstep" min="1.2345" step="1005.5545" value="2"> <input type="radio" required name="radiogroup" id="widgetradio"> <select required id="widgetselect"><option></option></select> <input type="url" id="widgeturl" value="a"> <input type="tel" id="widgettel" pattern="[0-9]{1}" value="a"> <!-- the rest not covered by the visually displayed ones --> <input type="hidden" id="ehidden"> <input type="image" id="eimage"> <input type="month" id="emonth"> <input type="password" id="epassword"> <input type="range" id="erange"> <input type="search" id="esearch"> <textarea id="etextarea"></textarea> <input type="week" id="eweek"> </div> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td></tr> </table> <!-- app lang --> <table id="tb4"> <col width="25%"><col width="75%"> <thead><tr><th colspan="2"> <div class="nav-title">application language &hellip; &ldquo; &rdquo; &lsquo; &rsquo; <div class="nav-up"><span class="c perf" id="locale"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="btn4 btnfirst" onClick="run()">[ run ]</span> </td></tr> <tr><td colspan="2"><hr><br></td></tr> <tr><td colspan="2" style="text-align: left;"> <div>CSS &nbsp; <span class='blue'>chrome://global/locale/intl.css</span> <p class="c mono spaces no_color" id="css"> &nbsp; </p> </div> <div>DIRECTION <p class="c mono spaces no_color" id="parsererror_direction"> &nbsp; </p> </div> <div>MEDIA MESSAGES <sup>1</sup> &nbsp; <a class='blue' href='https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/layout/MediaDocument.properties' target='blank'>searchfox MediaDocument</a> <p class="c mono spaces no_color" id="media_messages"> &nbsp; </p> </div> <div>REPORTING MESSAGES <p class="c mono spaces no_color" id="reporting_messages"> &nbsp; </p> </div> <div>VALIDATION MESSAGES &nbsp; <a class='blue' href='https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/dom/dom.properties' target='blank'>searchfox dom.properties</a> <p class="c mono spaces no_color" id="validation_messages"> &nbsp; </p> </div> <div>XML MESSAGES &nbsp; <a class='blue' href='https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/layout/xmlparser.properties' target='blank'>searchfox xmlparser</a> <p class="c mono spaces no_color" id="xml_messages"> &nbsp; </p> </div> <div>XSLT MESSAGES <p class="c mono spaces no_color" id="xslt_messages"> &nbsp; </p> </div> <div>XSLT SORT &nbsp; <a class='blue' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1978383' target='blank'>1978383: xsl:sort uses base sensitivity</a> <p class="c mono spaces no_color" id="xslt_sort"> &nbsp; </p> </div> </td></tr> <tr><td colspan="2" style="text-align: left;"> <div>NUMERIC INPUT &nbsp; <span class="c mono no_color" id="numericinputhash"></span> <p class="c mono spaces no_color" id="numeric_input"> &nbsp; </p> </div> </td></tr> <!--credits--> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><span class="no_color">code based on work by </span> <a target="_blank" class="blue" href="https://trac.torproject.org/projects/tor/ticket/30683">z3t on HackerOne</a> <sup>1</sup> </td> </tr> </table> <br> <script> 'use strict'; let isLocale // trigger some gecko deprecated items function set_reporting_items() { let aItems = ['InstallTrigger', 'fullScreen', 'onmozfullscreenchange', 'onmozfullscreenerror'] aItems.forEach(function(n) {try {window[n]} catch(e) {}}) try {screen.mozOrientation} catch(e) {} try {document.releaseCapture()} catch(e) {} } function get_css() { // ToDo: we also have chrome://global/content/widgets.css // e.g. chrome://global/skin/richlistbox.css const parent = dom.elementsdiv let oList = { "quote-fr": "<q lang='fr'></q>", } try { let oData = {}, target for (const k of Object.keys(oList).sort()) { // important to clear the div so no other elements can affect measurements parent.innerHTML = "" let data = [] try { parent.innerHTML = oList[k] //+" "+ k target = parent.children[0] oData[k] = [target.getBoundingClientRect().width, target.getBoundingClientRect().height] } catch(e) { console.log(e) oData[k] = zErr } } parent.innerHTML = "" let hash = mini(oData), notation = "" dom.css.innerHTML = hash + notation +"<br>"+ json_highlight(oData) return } catch(e) { parent.innerHTML = "" dom.css = e+"" return } } function get_direction() { let value try { let target = dom.tzpDirection target.innerHTML = '<parsererror></parsererror>' value = getComputedStyle(target.children[0]).direction } catch(e) { value = e } dom.parsererror_direction.innerHTML = value } function get_locale() { try { isLocale = Intl.DateTimeFormat().resolvedOptions().locale } catch(e) { // leave undefined } dom.locale.innerHTML = isLocale } function get_media_messages() { // https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/layout/MediaDocument.properties try { let mList = { "InvalidImage": "applang-invalid.png", // 0-byte file "ScaledImage": "applang-image.png", //"Unsupported": "applang-unsupported.png", } let mData = {} for (const k of Object.keys(mList)) { let target = dom[k], title = "" if (k === "ScaledImage") { title = target.contentWindow.document.title title = title.replace(mList[k], "") //strip image name to reduce noise } else { const image = target.contentWindow.document.querySelector('img') title = image.alt title = title.replace(target.src, "") // remove noise } title = title.trim() mData[k] = title } let hash = mini(mData) dom.media_messages.innerHTML = hash +"<br>"+ json_highlight(mData) return } catch(e) { dom.media_messages = e+"" return } } function get_reporting_messages() { if (!isFF) {return} let observer function exit(res) { try {observer.disconnect()} catch(e) {} if ('string' == typeof res) { dom.reporting_messages.innerHTML = res return } let max = 10 let aSet = new Set() for (let i=0; i < res.length; i++) { let msg = res[i].body.message msg = msg.replace('https://developer.mozilla.org/docs/Web/API/Element/releasePointerCapture','').trim() aSet.add(msg) if (max == aSet.size) {break} // reruns accrue messages so break } let data = Array.from(aSet).sort() if (data.length) { let hash = mini(data) dom.reporting_messages.innerHTML = hash +"<br>"+ json_highlight(data) } else { dom.reporting_messages.innerHTML = 'none' } } try { observer = new ReportingObserver((reports, observer) => {exit(reports)}, {types: ['deprecation'], buffered: true}) observer.observe() } catch(e) { exit(e+'') } } function get_numeric_inputs() { let target = dom.enumber let oTests = { arab: ['٤','٦'], arabext: ['۴','۶'], latn: ['4',4,'6',6,], mymr: ['၄','၆'], x: ['x'] } let oData = {} for (const k of Object.keys(oTests)) { oData[k] = {} oTests[k].forEach(function(n) { target.value = n let value = target.value let charName = "char", charValue, j = n if ("string" === typeof n) { charValue = n.charCodeAt(0) } else { charName = "number"; charValue = n; j = n +"n" } let validity = target.checkValidity() oData[k][j] = {} oData[k][j][charName] = charValue oData[k][j]["value"] = value oData[k][j]["validity"] = validity }) } let toggle = "num" dom.numericinputhash.innerHTML = mini(oData) +" <span id='labelhidden"+ toggle +"' class='btnfirst btn0' onClick=\"togglerows('hidden"+ toggle +"','expand')\">[ expand ]</span>" dom.numeric_input.innerHTML = "<span class='toghidden" + toggle +" hidden'>"+ json_highlight(oData) +"</span><br>" } function get_validation_messages() { // https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/dom/dom.properties // https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation // https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation // https://developer.mozilla.org/en-US/docs/Web/API/ValidityState // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input /* InvalidDate=Please enter a valid date. InvalidDateMonth=Please enter a valid month. InvalidDateTime=Please enter valid date and time. InvalidDateWeek=Please enter a valid week. InvalidTime=Please enter a valid time. PatternMismatchWithTitle=Please match the requested format: %S. StepMismatchOneValue=Please select a valid value. The nearest valid value is %S. TextTooLong=Please shorten this text to %S characters or less (you are currently using %S characters). TextTooShort=Please use at least %S characters (you are currently using %S characters). TimeReversedRangeUnderflowAndOverflow=Please select a value between %1$S and %2$S. */ // tooLong + tooShort constraints require the user: https://developer.mozilla.org/en-US/docs/Web/API/ValidityState/tooShort // ^ but we should add manual tests for those // InvalidDate/Time/Week/Month - can't set invalid values via JS const dList = { BadInputNumber: 'number', // this returns ValueMissing on chrome CheckboxMissing: 'checkbox', DateTimeRangeOverflow: 'datetime', DateTimeRangeUnderflow: 'datetimeunder', FileMissing: 'file', InvalidEmail: 'email', InvalidURL: "url", NumberRangeOverflow: 'max', NumberRangeUnderflow: 'min', PatternMismatch: 'tel', RadioMissing: 'radio', SelectMissing: 'select', StepMismatch: 'step', ValueMissing: 'text', } // dom let domvalue, domhash let dData = {} try { for (const k of Object.keys(dList).sort()) { try { let msg = dom["widget"+ dList[k]].validationMessage dData[k] = msg } catch(e) { dData[k] = zErr } } domhash = mini(dData) domvalue = domhash +" [DOM]<br>"+ json_highlight(dData) } catch(e) { domvalue = "DOM: "+ e+"" } // domparser let dommatch_green = sg+"[✓ matches DOM]"+sc let dommatch_red = sb+"[✗ matches DOM]"+sc let parservalue, parserhash const pList = { BadInputNumber: "<input type='number' required>", CheckboxMissing: "<input type='checkbox' required>", DateTimeRangeOverflow: "<input type='date' value='2024-01-01' max='2023-12-31'>", DateTimeRangeUnderflow: "<input type='date' value='2022-01-01' min='2023-12-31'>", FileMissing: "<input type='file' required>", InvalidEmail: "<input type='email' value='a'>", InvalidURL: "<input type='url' value='a'>", NumberRangeOverflow: "<input type='number' max='1974.3' value='2000'>", NumberRangeUnderflow: "<input type='number' min='8026.5' value='1'>", PatternMismatch: "<input type='tel' pattern='[0-9]{1}' value='a'>", RadioMissing: "<input type='radio' required name='radiogroup'>", SelectMissing: "<select required><option></option></select>", StepMismatch: "<input type='number' min='1.2345' step='1005.5545' value='2'>", ValueMissing: "<input type='text' required>", } let pData = {} try { let parser = new DOMParser for (const k of Object.keys(pList)) { try { let doc = parser.parseFromString(pList[k], 'text/html') let msg = doc.activeElement.children[0].validationMessage pData[k] = msg // +"b" // test alter hash } catch(e) { oData[k] = zErr } } parserhash = mini(pData) } catch(e) { parservalue = "DOMParser: "+ e+"" } if (parservalue == undefined) { // not an error if (parserhash == domhash) { parservalue = parserhash +" [DOMParser] "+ dommatch_green } else { parservalue = parserhash +" [DOMParser] "+ dommatch_red console.log("DOMParser\n", pData) parservalue += " check the console" } } dom.validation_messages.innerHTML = parservalue +"<br><br>"+ domvalue } function get_xml_messages() { if (!isFF) {return} // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString // https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction // https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/layout/xmlparser.properties // https://www.w3.org/TR/xml/ console.clear() const xmlList = { n02: "a", // syntax error n03: "", // no root element found n04: "<>", // not well-formed n05: "<", // unclosed token n07: "<x></X>", // mismatched tag n08: "<x x:x='' x:x=''>", // duplicate attribute n09: "<x></x><x", // junk after document element n11: "<x>&x;", // undefined entity n14: "<x>&#x0;", // reference to invalid character number n20: "<x><![CDATA[", // unclosed CDATA section n27: "<x:x>", // prefix not bound to a namespace n28: "<x xmlns:x=''></x>", // must not undeclare prefix n30: "<"+"?xml v=''?>", // XML declaration not well-formed // split "<"+"?" othersize notepad++s gets uposet with collapsing } let oMimeTypes = ['application/xml','application/xhtml+xml','image/svg+xml','text/xml',] // 'text/html', try { let xmlData = {}, delimiter = ":" oMimeTypes.forEach(function(mimetype) { let parser = new DOMParser let oTemp = {} for (const k of Object.keys(xmlList)) { try { let doc = parser.parseFromString(xmlList[k], mimetype) let str = (doc.getElementsByTagName('parsererror')[0].firstChild.textContent) let parts = str.split("\n") if ('n02' == k) { // ensure 3 parts: e.g. hebrew only has 2 lines let tmpStr = parts[1] let loc = window.location+'', locLen = loc.length, locStart = tmpStr.indexOf(loc) if (undefined == parts[2]) { let position = locLen+ locStart parts[1] = (tmpStr.slice(0, position)).trim() parts.push((tmpStr.slice(-(tmpStr.length - position))).trim()) } // set delimiter: should aways be the last item in parts[1] after we strip location // usually = ":" (charCode 58) but zh-Hans-CN = ":" (charCode 65306) and my = " -" let strLoc = (parts[1].slice(0, locStart)).trim() // trim delimiter = strLoc.slice(-1) // last char // concat some bits // don't trim strName prior to +delimiter (which is length 1) // e.g. 'fr','my' have a preceeding space, so capture that let strName = parts[0].split(delimiter)[0] + delimiter // use an object as joining for a string can get weird with RTL let oData = { 'delimiter': delimiter +' (' + delimiter.charCodeAt(0) +')', // redundant but record it for debugging 'error': strName, 'line': parts[2].trim(), 'location': strLoc, } oTemp['n00'] = oData } // parts[0] is always the error message let value = parts[0], trimLen = parts[0].split(delimiter)[0].length + 1 oTemp[k] = value.slice(trimLen).trim() } catch(err) { oTemp[k] = err+"" } } console.log(mimetype, oTemp) let hashTemp = mini(oTemp) if (xmlData[hashTemp] == undefined) { xmlData[hashTemp] = { "mimeTypes": [mimetype], "metrics": oTemp, } } else { xmlData[hashTemp]["mimeTypes"].push(mimetype) } }) let hash = mini(xmlData) dom.xml_messages.innerHTML = hash +"<br>"+ json_highlight(xmlData) console.clear() return } catch(e) { dom.xml_messages = e+"" console.clear() return } } function get_xslt_messages() { if (!isFF) {return} let hash, data ='' try { let data = {'xslt-parse-failure': dom.tzpXSLT.contentDocument.children[0].textContent} hash = mini(data) dom.xslt_messages.innerHTML = hash + '<br>'+ json_highlight(data) } catch(e) { dom.xslt_messages.innerHTML = e+'' } } function get_xslt_sort(aChars = []) { if (!isFF) {return} let data = {}, notation = '' let collatormatch_green = sg+"[✓ matches Intl.Collator]"+sc let collatormatch_red = sb+"[✗ matches Intl.Collator]"+sc try { // get characters let aData = [], oData = {} if (!aChars.length) { aChars = [ // already sorted 'A','a','aa','ch','ez','kz','ng','ph','ts','tt','y','\u00E2','\u00E4','\u00E7\a','\u00EB','\u00ED','\u00EE','\u00F0', '\u00F1','\u00F6','\u0107','\u0109','\u0137\a','\u0144','\u0149','\u01FB','\u025B','\u03B1','\u040E','\u0439','\u0453', '\u0457','\u04F0','\u0503','\u0561','\u05EA','\u0627','\u0649','\u06C6','\u06C7','\u06CC','\u06FD','\u0934','\u0935', '\u09A4','\u09CE','\u0A85','\u0B05','\u0B85','\u0C05','\u0C85','\u0D85','\u0E24','\u0E9A','\u10350','\u10D0','\u1208', '\u1780','\u1820','\u1D95','\u1DD9','\u1ED9','\u1EE3','\u311A','\u3147','\u4E2D','\uA647','\uFB4A' ] } // DONE: matches collator PoC aChars.sort() // always resort: default is already sorted, but in TZP we'll reuse the oIntl.collator.sort array //oData['chars'] = {'hash': mini(aChars.join(' , ').trim()), 'data': aChars.join(' , ').trim()} // build xslt aChars.forEach(function(item){aData.push('<a>'+ item +'</a>')}) const xData = '<'+'?xml version="1.0" encoding="UTF-8"?><doc>'+ aData.join('') +'</doc>' //console.log(mini(xData)) // 8f0f6080 // DONE: matches collator PoC let code = isLocale let control = aChars.sort(Intl.Collator(code, {usage: 'sort'}).compare).join(' , ').trim() // spaces before and to help LTR/RTL oData['collator'] = {'hash': mini(control), 'data': control} //console.log(xData) const xslText = '<'+'?xml version="1.0" encoding="UTF-8"?>' + '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' + '<xsl:template match="/"><xsl:for-each select="doc/a"><xsl:sort select="text()"/>' + '<xsl:value-of select="text()"/>,</xsl:for-each></xsl:template></xsl:stylesheet>' //console.log(mini(xslText)) // 97940905 const parser = new DOMParser() const xsltProcessor = new XSLTProcessor() const xslStylesheet = parser.parseFromString(xslText, "application/xml") xsltProcessor.importStylesheet(xslStylesheet) const xmlDoc = parser.parseFromString(xData, "application/xml"); const styledDoc = xsltProcessor.transformToDocument(xmlDoc); //console.log(styledDoc) let aTest = styledDoc.firstChild.textContent.split(/[\s,\n]+/); data = aTest.slice(0, -1) oData['xslt'] = {'hash': mini(data.join(' , ').trim()), 'data': data.join(' , ').trim()} let isMatch = oData.collator.hash == oData.xslt.hash if (isFF) { notation = isMatch ? collatormatch_green : collatormatch_red notation = '' } dom.xslt_sort.innerHTML = oData.xslt.hash +' '+ notation +"<br>"+ json_highlight(oData) } catch(e) { dom.xslt_sort = e+"" } } function run() { // clear let items = document.getElementsByClassName('c') for(let i=0; i < items.length; i++) { items[i].innerHTML = ' &nbsp; ' } // pause so users see change setTimeout(function() { get_locale() get_xml_messages() get_xslt_sort() get_numeric_inputs() get_validation_messages() get_media_messages() get_reporting_messages() get_direction() get_css() get_xslt_messages() }, 170) } Promise.all([ get_globals() ]).then(function(){ get_locale() if (isFF) { set_reporting_items() } else { let aList = ['reporting_messages','xml_messages','xslt_messages','xslt_sort'] aList.forEach(function(n) { let el = dom[n] el.innerHTML = 'gecko only' el.classList.remove('c') }) } }) </script> </body> </html> ================================================ FILE: tests/applang.xml ================================================ <!DOCTYPE html SYSTEM "chrome://global/locale/netError.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><meta charset="utf-8"/></head> <body><span id="DTD1">&loadError.label;</span> <script> window.addEventListener('message', (e) => { e.source.postMessage(document.getElementById('DTD1').innerText, '*'); }); </script> </body> </html> ================================================ FILE: tests/bridgemoji.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>bridge-moji</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} .darkmoji { color: white; background-color: #1c1b22 } .lightmoji { color: black; background-color: #ffffff } .largemoji { font-size: 16px; } .padmoji {line-height: 2} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <col width="20%"><col width="80%"> <thead><tr><th colspan="2"> <div class="nav-title">bridge-moji</div> </th></tr></thead> <tr><td colspan="2" class="mono" style="text-align: left; vertical-align: top;"> <span class="btn12 btnfirst" onClick="run()">[ run ]</span> <span class="btn12 btn" onClick="reset()">[ clear ]</span> <span class="btn12 btn" onClick="preset()">[ load preset ]</span> <span class="btn12 btn"> light <input id="rlight" type="radio" name="radio" value="lightmoji" onchange="handleTheme(this)"> dark <input id="rdark" type="radio" name="radio" value="darkmoji" onchange="handleTheme(this)" checked> </span><p> <span><span class="no_color">Preferred system font: </span> default <input id="default" value='default' type="radio" name="font" onchange="handleFont(this)"> Noto Color Emoji <input id="noto" value='noto' type="radio" name="font" onchange="handleFont(this)"> Twemoji <input id="twemoji" value='twemoji' type="radio" name="font" onchange="handleFont(this)"> </span> <br><br> <textarea rows="4" class="darkmoji" placeholder="comma delimited values: e.g: 🔥, ☎️, 🌽" style="width: 98%; resize: vertical;" id="valueE"></textarea> <br><br> <div class="spaces largemoji padmoji" id="results"></div> </td> </tr> </table> <br> <script> 'use strict'; let emojiPreset = [ "👽️", "🤖", "🧠", "👁️", "🧙", "🧚", "🧜", "🐵", "🦧", "🐶", "🐺", "🦊", "🦝", "🐱", "🦁", "🐯", "🐴", "🦄", "🦓", "🦌", "🐮", "🐷", "🐗", "🐪", "🦙", "🦒", "🐘", "🦣", "🦏", "🐭", "🐰", "🐿️", "🦔", "🦇", "🐻", "🐨", "🦥", "🦦", "🦘", "🐥", "🐦️", "🕊️", "🦆", "🦉", "🦤", "🪶", "🦩", "🦚", "🦜", "🐊", "🐢", "🦎", "🐍", "🐲", "🦕", "🐳", "🐬", "🦭", "🐟️", "🐠", "🦈", "🐙", "🐚", "🐌", "🦋", "🐛", "🐝", "🐞", "💐", "🌹", "🌺", "🌻", "🌷", "🌲", "🌳", "🌴", "🌵", "🌿", "🍁", "🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍", "🥭", "🍏", "🍐", "🍑", "🍒", "🍓", "🫐", "🥝", "🍅", "🫒", "🥥", "🥑", "🍆", "🥕", "🌽", "🌶️", "🥬", "🥦", "🧅", "🍄", "🥜", "🥐", "🥖", "🥨", "🥯", "🥞", "🧇", "🍔", "🍕", "🌭", "🌮", "🍿", "🦀", "🦞", "🍨", "🍩", "🍪", "🎂", "🧁", "🍫", "🍬", "🍭", "🫖", "🧃", "🧉", "🧭", "🏔️", "🌋", "🏕️", "🏝️", "🏡", "⛲️", "🎠", "🎡", "🎢", "💈", "🚆", "🚋", "🚍️", "🚕", "🚗", "🚚", "🚜", "🛵", "🛺", "🛴", "🛹", "🛼", "⚓️", "⛵️", "🛶", "🚤", "🚢", "✈️", "🚁", "🚠", "🛰️", "🚀", "🛸", "⏰", "🌙", "🌡️", "☀️", "🪐", "🌟", "🌀", "🌈", "☂️", "❄️", "☄️", "🔥", "💧", "🌊", "🎃", "✨", "🎈", "🎉", "🎏", "🎀", "🎁", "🎟️", "🏆️", "⚽️", "🏀", "🏈", "🎾", "🥏", "🏓", "🏸", "🤿", "🥌", "🎯", "🪀", "🪁", "🔮", "🎲", "🧩", "🎨", "🧵", "👕", "🧦", "👗", "🩳", "🎒", "👟", "👑", "🧢", "💄", "💍", "💎", "📢", "🎶", "🎙️", "📻️", "🎷", "🪗", "🎸", "🎺", "🎻", "🪕", "🥁", "☎️", "🔋", "💿️", "🧮", "🎬️", "💡", "🔦", "🏮", "📕", "🏷️", "💳️", "✏️", "🖌️", "🖍️", "📌", "📎", "🔑", "🪃", "🏹", "⚖️", "🧲", "🧪", "🧬", "🔬", "🔭", "📡", "🪑", "🧹", "🗿", ] let oFonts = {'default': '', noto: 'Noto Color Emoji', twemoji: 'Twemoji Mozilla'} function reset() { dom.valueE.value = "" } function preset() { dom.valueE.value = emojiPreset.join(", ") } preset() function handleTheme(src) { let remove = (src.value == "darkmoji" ? "lightmoji" : "darkmoji") dom.valueE.classList.add(src.value) dom.valueE.classList.remove(remove) dom.results.classList.add(src.value) dom.results.classList.remove(remove) } function handleFont(src) { let fnt = oFonts[src.value] dom.valueE.style.fontFamily = fnt dom.results.style.fontFamily = fnt } dom.rdark.checked = true dom.default.checked = true function run_random(array) { // let data = [] for (let i=0; i < 500; i++) { let arr = [], line = [] while(arr.length < 4){ let r = Math.floor(Math.random() * array.length) if(arr.indexOf(r) === -1) {arr.push(r); line.push(array[r]) } } data.push(line.join("")) } // group into lines function groupLines([a,b,c,d,...rest]){ if (rest.length === 0) return [[a,b,c,d].filter(x => x!==undefined)] return [[a,b,c,d]].concat(groupLines(rest)) } let newData = groupLines(data) // display let display = [] newData.forEach(function(array){ display.push(array.join(" ")) }) dom.results.innerHTML = display.join("<br>") } function run() { // reset dom.results = "" let emojiList = [] // clean up the list let valueE = (dom.valueE.value).trim() valueE = valueE.replace(/['"]+/g, "") valueE = valueE.trim() if (valueE.length) { let tmpArr = valueE.split(",") for (let i = 0 ; i < tmpArr.length; i++) { let trimmed = tmpArr[i].trim() if (trimmed.length) { emojiList.push(trimmed) } } // make sure we have at least four unique items emojiList = emojiList.filter(function (item, position) { return emojiList.indexOf(item) === position }) if (emojiList.length < 4) { dom.results = "aww, snap! try adding some "+ (emojiList.length > 0 ? "more " : "") +"emojis" return } } else { dom.results = "aww, snap! try adding some emojis" return } //console.debug(emojiList) run_random(emojiList) } </script> </body> </html> ================================================ FILE: tests/canvasnoise.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>canvas spoof fingerprinting</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#canvas">return to TZP index</a></td></tr> </table> <table id="tb9"> <col width="18%"><col width="82%"> <thead><tr><th colspan="2"> <div class="nav-title">canvas spoof fingerprinting <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Creates a random canvas with known results, reads it back, and analyzes the differences. <code>getImageData</code> is being tested, but <code>toDataURL</code> and <code>toBlob</code> can also be "decoded" (albeit a little differently)</span> </td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2"> <div class="btn-left"> <span class="btn9 btn" onClick="run()">[ re-run ]</span> <span><input id="optSolid" type="checkbox"> use a solid color</span> <span><input id="optStroke" type="checkbox"> use strokeText</span><br><br> </div>VISUALS</td></tr> <tr><td class="bottom padr">control</td> <td> <canvas id="cnvBig" width="180" height="180" style="border:5px solid white;"></canvas> &nbsp; <canvas id="cnvCtrl" width="20" height="20" style="border:1px solid white;"></canvas> &nbsp; <span class="s9"> &#9664; what we set</span> </td></tr> <tr><td></td><td><span class="intro">The control that we read values from, pixel by pixel</span></td></tr> <tr><td colspan="2"></td></tr> <!-- spacer --> <tr><td class="bottom padr">1st read<br>getImageData</td> <td> <canvas id="cnvBig2" width="180" height="180" style="border:5px solid white;"></canvas> &nbsp; <canvas id="cnvGet" width="20" height="20" style="border:1px solid white;"></canvas> &nbsp; <span class="s9"> &#9664; what we got back</span></td></tr> <tr><td></td><td><span class="intro">What we have read back from the control, pixel by pixel</span></td></tr> <tr><td colspan="2"></td></tr> <!-- spacer --> <tr> <td class="bottom padr">results</td> <td> <span class="s9"> <canvas id="cnvGet2" width="20" height="20" style="border:1px solid white;" src=""></canvas> &nbsp; &#9664; getImageData &nbsp; <img id="cnvURL" width="20" height="20" style="border:1px solid white;" src=""> &nbsp; &#9664; toDataURL &nbsp; <img id="cnvBlob" width="20" height="20" style="border:1px solid white;" src=""> &nbsp; &#9664; toBlob </span> </td> </tr> <tr><td colspan="2"></td></tr> <!-- spacer --> <tr><td colspan="2"></td></tr> <!-- spacer --> <!-- DATA --> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2">DATA ANALYSIS</td></tr> <tr><td class="padr">control</td><td class="c mono" id="sethash"></td></tr> <tr><td class="padr">1st read</td><td class="c mono" id="readhash"></td></tr> <tr><td class="padr">2nd read</td><td class="c mono" id="readhash2"></td></tr> <tr><td class="padr">stats</td><td class="c mono" id="readstats"></td></tr> <tr><td class="padr">diffs</td><td class="c mono" id="readdiff"></td></tr> </table> <br> <script> 'use strict'; function run() { // vars let clrSet = [], cellSet = [], clrRead = [], clrRead2 = [], cellRead = [], cellRead2 = [], hashSet = "", hashGetImage = "", hashGetImage2 = "", cWidth = 20, cHeight = 20, m = 9 // multplier for large visuals // clear let items = document.getElementsByClassName("c") for(let i=0; i < items.length; i++) { items[i].innerHTML = "&nbsp" } let optSolid = dom.optSolid.checked let optStroke = dom.optStroke.checked // pause so users see change setTimeout(function(){ try { let t0 = performance.now() let cnvBig = dom.cnvBig let cnvCtrl = dom.cnvCtrl if (cnvBig.getContext) { let ctxBig = cnvBig.getContext('2d'), ctxCtrl = cnvCtrl.getContext('2d'), ctxGet = dom.cnvGet.getContext('2d'), ctxGet2 = dom.cnvGet2.getContext('2d'), ctxBig2 = dom.cnvBig2.getContext('2d') // note: all random values we don't use 255 so RFP-white shows up in all it's glory let solidR = Math.floor(Math.random()*255), solidG = Math.floor(Math.random()*255), solidB = Math.floor(Math.random()*255) let solidClrs = solidR +", "+ solidG +", "+ solidB +", 255" // fill big visual with our random color ctxBig.fillStyle = "rgba("+ solidClrs +")" ctxBig.fillRect(0, 0, cnvBig.width, cnvBig.height); // ensure background is correct color in ctrl ctxCtrl.fillStyle = "rgba("+ solidClrs +")" ctxCtrl.fillRect(0, 0, cnvCtrl.width, cnvCtrl.height); if (optSolid) { let total = cWidth * cHeight for (let i=0; i < total; i++) { clrSet.push(solidR, solidG, solidB, 255) cellSet.push(solidClrs) } } let indexFont = [] if (!optSolid) { let clrR = Math.floor(Math.random()*255), clrG = Math.floor(Math.random()*255), clrB = Math.floor(Math.random()*255) let fpText = "\u2588\u2588\u2588\u2588" // full block // order matters // trigger fillText stealth // only the text area is altered, so try and make it cover all of it ctxCtrl.font = "512px sans-serif" // large ctxCtrl.textBaseline = "top" ctxCtrl.textBaseline = "alphabetic" ctxCtrl.fillText(fpText,0,0) // trigger strokeText stealth: don't overwrite half the fillText if (optStroke) { fpText = "-" // straight, less curves ctxCtrl.font = "16px monospace" // even straighter ctxCtrl.strokeStyle ="rgba("+ solidClrs +")" for (let x=0; x < cWidth/2; x++) { for (let y=0; y < cHeight/4; y++) { // divide by 4 to match TZP size ctxCtrl.strokeText(fpText,x,y) } } } /* results fillText only 49 : CB = has at least fillText fillText + strokeText 49 : CB = only fillText 49 : CB = has fillText + strokeText ~14 : CB = only strokeText 0 : false positive: can't get any on my machine but see below strokeText can create false positives (aliasing?, bezier curves etc) on my machine: windows, dpi=1, en* language default fonts, default text size etc - 10k tests on TZP main: `-` char and monospace = zero false positives zoom doesn't seem to affect it on my machine running 100 tests: false positives 'a' monospace = 98% (9 affected cells) 'x' monospace = 84% (9) '|' monospace = 75% (5) '-' monospace = 0% = also 0 with 10k tests 'a' sans-serif = 99% (7 affected cells overall) 'x' sans-serif = 94% (7) '|' sans-serif = 51% (5) '-' sans-serif = 16% (1) Too risky to use in production given all the variables across platforms/users e.g. even if we decided that if only 1 (or 2) indexFonts changed and if only indexFonts changed, i.e it is in stealth mode, means a false positive: at the end of the day we really can't be sure. We need to deal in absolutes */ // then overwrite a % of the pixels with random values let counter = -1 for (let x=0; x < cWidth; x++) { let xEven = (x % 2 == 0) for (let y=0; y < cHeight; y++) { counter ++ let k = counter * 4 let yEven = (y % 2 == 0) // xEven + yEven == 1 = checkerboard = 1/2 // xEven + yEven == 2 = another 1/4 // xEven + yEven == 0 = the remainder: of which we can further reduce e.g. multples of 3 let go = (xEven + yEven == 1 || xEven + yEven == 2) // 3/4ths if (!go) { if ((x * y) % 3 == 0 ) {go = true} // brings us up to 351/400 } if (go) { // get random color clrR = Math.floor(Math.random()*255) clrG = Math.floor(Math.random()*255) clrB = Math.floor(Math.random()*255) let clrs = clrR +", "+ clrG +", "+ clrB +", 255" clrSet.push(clrR) clrSet.push(clrG) clrSet.push(clrB) clrSet.push(255) cellSet.push(clrs) ctxBig.fillStyle = "rgba("+ clrs +")" ctxBig.fillRect(x*m, y*m, m, m) ctxCtrl.fillStyle = "rgba("+ clrs +")" ctxCtrl.fillRect(x, y, 1, 1) } else { indexFont.push(k) //don't touch the canvas but record solid colors clrSet.push(solidR) clrSet.push(solidG) clrSet.push(solidB) clrSet.push(255) cellSet.push(solidClrs) } } } } hashSet = mini(clrSet) +" | "+ mini(cellSet) dom.sethash = hashSet // getImageData let imageData = ctxCtrl.getImageData(0,0, cnvCtrl.width, cnvCtrl.height) ctxGet2.putImageData(imageData, 0, 0) // toDataURL let dataURL = cnvCtrl.toDataURL("image/png") cnvURL.src = dataURL // toBlob cnvCtrl.toBlob(function(blob) { let url = URL.createObjectURL(blob) cnvBlob.src = url }) // 1st read let testRead = ctxCtrl.getImageData(0,0, cWidth, cHeight).data let aRead = [] for (let x=0; x < cWidth; x++) { for (let y=0; y < cHeight; y++) { // we need to read x/y as the opposite // as getImageData reads down then across // so we want the 0th quartet, then 20th, then 40th let k = x + (y * cHeight) aRead = testRead.slice(k*4, (k*4) + 4) clrRead.push(aRead[0]) clrRead.push(aRead[1]) clrRead.push(aRead[2]) clrRead.push(aRead[3]) let pixel = aRead.join(", ") cellRead.push(pixel) // output READ visuals: just on the first read ctxBig2.fillStyle = "rgba("+ pixel +")" ctxBig2.fillRect(x*m, y*m, m, m) ctxGet.fillStyle = "rgba("+ pixel +")" ctxGet.fillRect(x, y, 1, 1) } } hashGetImage = mini(clrRead) +" | " + mini(cellRead) let indexChanged = [] for (let i=0; i < cellSet.length; i++) { if (cellSet[i] !== cellRead[i]) { indexChanged.push(i * 4) } } let aNotInFonts = indexChanged.filter(x => !indexFont.includes(x)) let isInputOnly = aNotInFonts == 0 // 2nd read let testRead2 = ctxCtrl.getImageData(0,0, cWidth, cHeight).data let aRead2 = [] for (let x=0; x < cWidth; x++) { for (let y=0; y < cHeight; y++) { // we need to read x/y as the opposite // as getImageData reads down then across // so we want the 0th quartet, then 20th, then 40th let k = x + (y * cHeight) aRead2 = testRead2.slice(k*4, (k*4) + 4) clrRead2.push(aRead2[0]) clrRead2.push(aRead2[1]) clrRead2.push(aRead2[2]) clrRead2.push(aRead2[3]) let pixel = aRead2.join(", ") cellRead2.push(pixel) } } hashGetImage2 = mini(clrRead2) +" | " + mini(cellRead2) // output if (hashGetImage2 == hashSet) { dom.readhash2.innerHTML = hashGetImage2 + s9 +" [matches]"+ sc } else { dom.readhash2.innerHTML = sb + hashGetImage2 + sc + (hashGetImage2 == hashGetImage ? " [cached]" : " [per-execution]") } if (hashGetImage == hashSet) { dom.readhash.innerHTML = hashGetImage + s9 +" [matches]"+ sc } else { dom.readhash.innerHTML = sb + hashGetImage + sc // analyze let changeR = [], changeG = [], changeB = [], changeA = [], changeC = [], changeC2 = [], channels = [] let absR = [], absG = [], absB = [] let negR = 0, negG = 0, negB = 0, z = 0 let chan = "" for (let i=0; i < clrSet.length; i++) { let diff = clrRead[i] - clrSet[i] if (z==0) { if (diff !== 0) {chan += "r"; changeR.push(diff); absR.push(Math.abs(diff)); if(diff < 0) {negR++}} z = 1 } else if (z==1) { if (diff !== 0) {chan += "g"; changeG.push(diff); absG.push(Math.abs(diff)); if(diff < 0) {negG++}} z = 2 } else if (z==2) { if (diff !== 0) {chan += "b"; changeB.push(diff); absB.push(Math.abs(diff)); if(diff < 0) {negB++}} z = 3 } else { if (diff !== 0) {chan += "a"; changeA.push(diff)} if (chan !== "") {channels.push(chan)} z = 0 chan = "" } } // what co-ordinates changed for (let i=0; i < cellSet.length; i++) { if (cellSet[i] !== cellRead[i]) {changeC.push(i)} if (cellSet[i] !== cellRead2[i]) {changeC2.push(i)} } /* if (changeC.length > 1) { console.debug("read 1: pixels changed: ", mini(changeC) +"\n", changeC) } if (changeC2.length > 1) { console.debug("read 2: pixels changed: ", mini(changeC2) +"\n", changeC2) } */ // channels let counts = {} channels.forEach(function(c) { counts[c] = (counts[c] || 0) + 1 }) let tmpchan = channels.filter(function(item, position) {return channels.indexOf(item) === position}) tmpchan.sort() let chancount = 0, chanstring = "" if (changeR.length) {chancount++; chanstring = "r"} if (changeG.length) {chancount++; chanstring += "g"} if (changeB.length) {chancount++; chanstring += "b"} if (changeA.length) {chancount++; chanstring += "a"} // stats dom.readstats.innerHTML = " cells changed: "+ changeC.length +"<br> channels changed: "+ chancount +" [" + chanstring +"]" +"<br> channel counts: r: "+ changeR.length +", g: "+ changeG.length +", b: "+ changeB.length +", a: "+ changeA.length +"<br> combos altered: "+ tmpchan.length +" ["+ tmpchan.join(", ") +"]" +"<br> combo counts: "+ JSON.stringify(counts) +"<br> stealth mode: "+ (isInputOnly ? sb +"yes"+ sc +" [input only changed]" : "no") // diffs let tmpR = changeR.filter(function(item, position) {return changeR.indexOf(item) === position}) let tmpG = changeG.filter(function(item, position) {return changeG.indexOf(item) === position}) let tmpB = changeB.filter(function(item, position) {return changeB.indexOf(item) === position}) let tmpA = changeA.filter(function(item, position) {return changeA.indexOf(item) === position}) // sort diffs numerically tmpR.sort(function(a, b){return a-b}) tmpG.sort(function(a, b){return a-b}) tmpB.sort(function(a, b){return a-b}) tmpA.sort(function(a, b){return a-b}) // for each we want a count and a spread (max/min) // +/- split (ignore a) let strR = "n/a", strG = "n/a", strB = "n/a", strA = "n/a" if (tmpR.length) { strR = tmpR.length +" ["+ tmpR[0] +" to "+ tmpR[tmpR.length-1] +", "+ (tmpR[tmpR.length-1]-tmpR[0]) +"]" strR += " "+ negR +"/"+ (changeR.length-negR) } if (tmpG.length) { strG = tmpG.length +" ["+ tmpG[0] +" to "+ tmpG[tmpG.length-1] +", "+ (tmpG[tmpG.length-1]-tmpG[0]) +"]" strG += " "+ negG +"/" + (changeG.length-negG) } if (tmpB.length) { strB = tmpB.length +" ["+ tmpB[0] +" to "+ tmpB[tmpB.length-1] +", "+ (tmpB[tmpB.length-1]-tmpB[0]) +"]" strB += " "+ negB +"/"+ (changeB.length-negB) } if (tmpA.length) { strA = tmpA.length +" ["+ tmpA[0] +" to "+ tmpA[tmpA.length-1] +", "+ (tmpA[tmpA.length-1]-tmpA[0]) +"]" } // absolute stats (ignore a) tmpR = absR.filter(function(item, position) {return absR.indexOf(item) === position}) tmpG = absG.filter(function(item, position) {return absG.indexOf(item) === position}) tmpB = absB.filter(function(item, position) {return absB.indexOf(item) === position}) tmpR.sort(function(a, b){return a-b}) tmpG.sort(function(a, b){return a-b}) tmpB.sort(function(a, b){return a-b}) if (tmpR.length) { strR += " ... " + tmpR.length +" ["+ tmpR[0] +" to "+ tmpR[tmpR.length-1] +", "+ (tmpR[tmpR.length-1]-tmpR[0]) +"]" } if (tmpG.length) { strG += " ... " + tmpG.length +" ["+ tmpG[0] +" to "+ tmpG[tmpG.length-1] +", "+ (tmpG[tmpG.length-1]-tmpG[0]) +"]" } if (tmpB.length) { strB += " ... " + tmpB.length +" ["+ tmpB[0] +" to "+ tmpB[tmpB.length-1] +", "+ (tmpB[tmpB.length-1]-tmpB[0]) +"]" } dom.readdiff.innerHTML = " r: "+ strR +"<br> g: "+ strG +"<br> b: "+ strB +"<br> a: "+ strA } // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } } catch(e) { dom.readhash.innerHTML = sb + e.name +": "+ sc + e.message } }, 170) } run() </script> </body> </html> ================================================ FILE: tests/canvasrfp.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=620"> <title>canvas rfp</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 600px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#canvas">return to TZP index</a></td></tr> </table> <table id="tb9"> <col width="1%"><col width="99%"> <thead><tr><th colspan="2"> <div class="nav-title">canvas RFP <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="s9">FF96+ </span> <span class="no_color">: testing RFP characteristics in toDataURL [and by extension toBlob]</span> &nbsp; &#x25BA; <canvas id="canvas" width="16" height="16"></canvas> &nbsp; &#x25C4; <br><br> <details><summary><span class="no_color">click me for more info</span></summary> <span class="no_color"> <ul> <li>toDataURLs: <code>aRaw</code></li> <li>change canvas dimensons: max 16, min 2: <code>set_size(width, height)</code></li> <li>all coded patterns: <code>oControl</code></li> <li>running a size with no patterns will still summarize possible RFP rules</li> </ul> </span> </details> </td></tr> <tr><td colspan="2" class="mono" style="text-align: left; vertical-align: top;"> <span class="btn9 btnfirst" onClick="run_checks(100)">[ run 100 ]</span> <span class="btn9 btn" onClick="run_checks(500)">[ run 500 ]</span> <span class="btn9 btn" onClick="run_checks(1000)">[ run 1000 ]</span> <span class="btn9 btn" onClick="run_checks(5000)">[ run 5000 ]</span> <span class="btn9 btn" onClick="run_checks(20000)">[ run 20000 ]</span> <br><br><hr> <br><span class="spaces" style="color: #b3b3b3;" id="totals"></span> <br><span class="spaces" style="color: #b3b3b3;" id="results"></span> </td> </tr> <tr><td colspan="2"></td></tr> <!-- spacer --> </table> <br> <script> 'use strict'; // data let aBypass = [], aRaw = [], oFail = {}, oPass = {}, oRules = {} //counts let countTest = 0, runningTotal = 0, runningPass = 0, runningMatch = 0, maxLines = 20, oRunningTotal = {} // control let controlHash = "", controlLengths = [], testA = "", testB = "", hashA = "", hashB = "", hasRules = false // FF96+ expected values: see 1724331 + 1737038 let oControl = { "16x16": { "hash": "bdcce913", "lengths": [174,178,182,186,190], "ruleNos": {}, "rules": {}, "slice1": "lEQVQ4T2", "slice2": ["5ErkJggg==","VORK5CYII=","lFTkSuQmCC"], }, "16x8": { "hash": "a8d0bd06", "lengths": [166,170,174,178], "ruleNos": {}, "rules": {}, "slice1": "lEQVQoU2", "slice2": ["5ErkJggg==","VORK5CYII=","lFTkSuQmCC"], }, } function populate_rules() { for (const k of Object.keys(oControl)) { let ruleNo = 1 let lengths = oControl[k]["lengths"], slice1 = oControl[k]["slice1"], slice2 = oControl[k]["slice2"] lengths.forEach(function(len) { slice2.forEach(function(slice) { let key = len +"..."+ slice1 +"..."+ slice oControl[k]["rules"][key] = ruleNo oControl[k]["ruleNos"][ruleNo] = key ruleNo++ }) }) } } let is96 = (Intl.PluralRules.supportedLocalesOf("sc").join() == "sc") let sizeW = 16, sizeH = 8 // smaller faster test function run_checks(max) { /* STEP ONE run some checks */ if ("undefined" === typeof Object.toSource && "sc" === Intl.PluralRules.supportedLocalesOf("sc").join()) { } else { // 95 or lower dom.results.innerHTML = "requires Firefix 96+" return } // reset dom.perf = "" dom.results = "" countTest = max aBypass = [] if (runningTotal == 0) { // reset dom.totals = "" oRunningTotal = {} oRules = {} hasRules = false try { // pre-generate arrays if (oControl[sizeW +"x"+ sizeH] !== undefined) { for (const k of Object.keys(oControl[sizeW +"x"+ sizeH]["rules"])) { let ruleNo = oControl[sizeW +"x"+ sizeH]["rules"][k] ruleNo = "rule" + (ruleNo +"").padStart(2, "0") oRunningTotal[ruleNo] = [0] oRules[k] = oControl[sizeW +"x"+ sizeH]["rules"][k] } hasRules = true } //console.log(oRunningTotal) //console.log(oRules) } catch(e) {} } // isFF if (!isFF) {aBypass.push("this is not gecko")} // FF78+ if (isFF && !window.Document.prototype.hasOwnProperty("replaceChildren")) { aBypass.push("this is not Firefox 78+, when RFP canvas was added") } if (isFF && !is96) { aBypass.push("this is not Firefox 96+, as per the test description") } // isFile //if (isFF && isFile) {aBypass.push("RFP doesn't work on file scheme")} // heh if (runningTotal >= 500000) {dom.results = "that's enough tests for now... go take a break"; return} // create canvas once per run try { let canvas = document.getElementById("canvas") canvas.width = sizeW canvas.height = sizeH // always reset canvas size since width+height are variable via console let ctx = canvas.getContext('2d') for (let x=0; x < sizeW; x++) { for (let y=0; y < sizeH; y++) { ctx.fillStyle = "rgba(" + (x*y) +","+ (x*16) +","+ (y*16) +",255)" ctx.fillRect(x, y, 1, 1) } } } catch(e) { dom.results.innerHTML = e.name === undefined ? "error" : sb+ e.name + sc+": " + e.message return } // looks good get_rawdata() } function get_rawdata() { /* STEP TWO get raw data */ // info dom.results = "calculating..." // reset oFail = {}, oPass = {} aRaw = [] controlHash = "" controlLengths = [] testA = "" testB = "" hashA = "" hashB = "" // test away let delay = 250 if (countTest > 1000) {delay = 45} setTimeout(function(){ try { let t0 = performance.now() let canvas = document.getElementById("canvas") // first two tests testA = canvas.toDataURL() hashA = mini(testA) testB = canvas.toDataURL() hashB = mini(testA) if (testA === testB) {aBypass.push("canvas is not random per execution")} // FF if (isFF) { try { controlHash = oControl[sizeW+"x"+sizeH]["hash"] controlLengths = oControl[sizeW+"x"+sizeH]["lengths"] if (hashA === controlHash || hashB === controlHash) {aBypass.push("canvas is not being spoofed")} } catch(e) { //console.error(e.name, e.message) // don't return if missing control data, we want to allow different sizes in aRaw } /* console.log(sizeW +" x "+ sizeH) console.log(testA.length, testA) console.log(testB.length, testB) console.log(aBypass) console.log(controlHash) console.log(controlLengths) //*/ } // add existing two tests aRaw.push(testA, testB) // get the rest for (let i=2; i < countTest; i++) { aRaw.push(canvas.toDataURL()) } // raw perf let t1 = performance.now() let perItem = (t1-t0)/countTest if (!Number.isInteger(perItem)) {perItem = perItem.toFixed(2)} dom.perf.innerHTML = Math.round(t1-t0) +" ms | "+ perItem +" each" // next step analyse_rawdata() } catch(e) { dom.results.innerHTML = (e.name === undefined ? "error" : sb + e.name + sc +": "+ e.message) } }, delay) } function analyse_rawdata() { /* STEP THREE analyze raw data */ //console.log("'" + aRaw.join("',\n'") + "'") try { // get occurences of each let oOccurrences = aRaw.reduce(function(occ, item) { occ[item] = (occ[item] || 0) + 1 return occ }, {}) // keep counts by slice data + length // keep counts by slice data + length for (const k of Object.keys(oOccurrences)) { let len = k.length let count = oOccurrences[k] let str // default slices let slice1 = k.slice(72,80), slice2 = k.slice(len - 10, len) if (hasRules) { let key = len +"..."+ slice1 +"..."+ slice2 if (Object.keys(oRules).includes(key)) { str = oRules[key] } } if (str !== undefined) { if (oPass[str] === undefined) { oPass[str] = count } else { oPass[str] = oPass[str] + count } } else { str = len +"..."+ slice1 +"..."+ slice2 if (oFail[str] === undefined) { oFail[str] = count } else { oFail[str] = oFail[str] + count } } } // next output() } catch(e) { console.error(e.name, e.message) } } function output() { let display = [] display.push(s9 +"TESTS RUN: " + countTest + sc +" ["+ sizeW +"w x "+ sizeH +"h]<br>" ) // tests /* simulate RFP oPass = {"7": countTest - 33, "11": 33} oFail = {} aBypass = [] //*/ /* simulate 100% matches but one basic check fails oPass = {"7": countTest - 33, "11": 33} oFail = {} aBypass = ["canvas is not random per execution"] //*/ /* simulate 100% matches but multiple basic check fails oPass = {"7": countTest - 33, "11": 33} oFail = {} aBypass = ["RFP is not enabled","canvas is not random per execution"] //*/ /* simulate mixed: 1 nonmatch oPass = {"7": countTest - 33, "11": 32} oFail = { "274...lEQVQ4T5...VORK5CYII=": 1 } aBypass = [] //*/ /* simulate mixed: 1 nonmatch + bypasses oPass = {"7": countTest - 33, "11": 32} oFail = { "274...lEQVQ4T5...VORK5CYII=": 1 } aBypass = ["RFP is not enabled","canvas is not random per execution"] //*/ /* simulate mixed: multi nonmatch oPass = {"7": countTest - 33, "11": 25} oFail = { "274...lEQVQ4T5...VORK5CYII=": 3, "280...BANANANA...WOOOOWEEE=": 5, } aBypass = [] //*/ /* simulate mixed: multi exceed maxLines oPass = {"7": countTest - 33, "11": 23} // 10 fails oFail = { "274...lEQVQ4T5...VORK5CYII=": 1, "280...BANANANA...WOOOOWEEE=": 2, "260...==TOR===...==BROWSER=": 1, // maxLines "226...TZPTZPTZ...TZPTZPTZP=": 1, // sum the rest "274...aEQVQ4T5...aORK5CYIa=": 1, "274...bEQVQ4T5...bORK5CYIb=": 1, "274...cEQVQ4T5...cORK5CYIc=": 2, "274...dEQVQ4T5...dORK5CYId=": 1, } maxLines = 3 //*/ let countMatch = 0, // matches pattern countPass = 0 // legit passes let strBypass = "" aBypass.forEach(function(item) {strBypass += "<li>"+ item +"</li>"}) if (strBypass !== "") {strBypass = "<ul>"+ strBypass + "</ul>"} for (const k of Object.keys(oPass)) { let count = oPass[k] countMatch += count let rule = oControl[sizeW +"x" + sizeH]["ruleNos"][k] display.push(("RULE"+ k).padEnd(6) + " : " + rule +" : "+ s9 + (oPass[k] +"").padStart(5) + sc) // update running match totals let curTotal = oRunningTotal["rule"+ (k +"").padStart(2,"0")][0] let newTotal = curTotal + oPass[k] oRunningTotal["rule"+ (k +"").padStart(2,"0")] = [newTotal] // track LEGIT pass if (aBypass.length == 0) {countPass += oPass[k]} } let percentMatch = ((countMatch/countTest) * 100).toFixed(2) // MATCHES if (countMatch > 0) { display.push(s9 +"".padStart(39) + "-----" + sc) display.push(s9 +"TOTAL PATTERN MATCHES : ".padStart(39) + (countMatch +"").padStart(5) + sc) } // NON-MATCHES let countFail = countTest - countMatch if (countFail > 0) { if (countMatch > 0) {display.push("<br>")} if (percentMatch == 100) {percentMatch = ((countMatch/countTest) * 100).toFixed(3)} // ensure under 100.00 // only display the first x lines, sum the rest let countLine = 0, countRemainder = 0 for (const k of Object.keys(oFail)) { let countItem = oFail[k] if (countLine < maxLines) { countItem = countItem.toString() display.push(k + sb + countItem.padStart(17) + sc) } else { countRemainder += countItem } countLine ++ } if (countRemainder > 0) { let keysRemaining = Object.keys(oFail).length - maxLines let summary = keysRemaining +" other" + (keysRemaining > 1 ? "s" : "") display.push(summary.padStart(39) + sb + (countRemainder +"").padStart(5) + sc) } display.push(sb +"".padStart(39) + "-----" + sc) display.push(sb +"TOTAL NON-MATCHES : ".padStart(39) + (countFail +"").padStart(5) + sc) // START ANALYSIS display.push("<br><hr><br>"+ s9 +"ANALYSIS: "+ sc + sb +"\u2715 FAILED"+ sc +" | PATTERN MATCHES "+ sb + percentMatch +"%"+ sc +" | PASSES " + (countPass === 0 ? sb +"ZERO" : s9 + countPass) + sc ) } // ANALYSIS if (countMatch == countTest) { if (aBypass.length) { // e.g. RFP is on, but has canvas site exception and an // extension matches RFP canvas but does not randomize per execution display.push("<br><hr><br>"+ s9 +"ANALYSIS: "+ sc + sb +"\u2715 FAILED"+ sc +" | PATTERN MATCHES "+ s9 + percentMatch +"%"+ sc +" | PASSES "+ sb +"ZERO"+ sc) } else { // we missed some control rules? display.push("<br><hr><br>"+ s9 +"ANALYSIS: \u2713 NAILED"+ sc +" | PATTERN MATCHES "+ s9 + percentMatch +"%"+ sc +" | PASSES "+ s9 + countTest + sc) } } // BYPASS INFO if (aBypass.length) { display.push("<br>You "+ sb + (countMatch == countTest ? "": "also ") +"failed"+ sc +" to pass " + (aBypass.length == 1 ? "this basic check" : "these basic checks") +", so " + sb +"no soup for you!"+ sc +"<br><br>" + strBypass ) } // OUTPUT dom.results.innerHTML = display.join("<br>") // RUNNING TOTALS runningTotal += countTest runningPass += countPass runningMatch += countMatch let percentTotalMatch = ((runningMatch/runningTotal) * 100).toFixed(2) let percentTotalPass = ((runningPass/runningTotal) * 100).toFixed(2) let strTotalMatch = (percentTotalMatch < 100 ? sb : s9 ) + runningMatch +" ["+ percentTotalMatch +"%]" + sc let strTotalPass = (percentTotalPass < 100 ? sb : s9 ) + runningPass + " ["+ percentTotalPass +"%]"+ sc let strTotalTests = s9 +"RUNNING TOTALS: "+ sc +"TESTS "+ s9 + runningTotal + sc let strPatternInfo = "<br><br>\u25BC PATTERN MATCHES ["+ sizeW +"w x "+ sizeH +"h]<br>" let str = strTotalTests +" | MATCHES " + strTotalMatch +" | PASSES " + strTotalPass + strPatternInfo display = [str] if (!hasRules) {display.push("note: there are no pattern rules for this size")} let displayTotal = [] for (let i=1; i < Object.keys(oRules).length +1; i++) { let curTotal = oRunningTotal["rule"+ (i +"").padStart(2,"0")][0] let strTotal = curTotal +"" if (strTotal.length > 5) {strTotal = "> 99999"} strTotal = strTotal.padStart(7) let ruleNo = "RULE"+ (i +"").padEnd(2) strTotal = curTotal == 0 ? ruleNo +": "+ strTotal : s14 + ruleNo + sc +": "+ strTotal displayTotal.push(strTotal) if (((i-1) % 4) == 3 || i == Object.keys(oRules).length) { // 4 per line plus final line display.push(displayTotal.join(" | ")) displayTotal = [] } } display.push("<br><hr>") dom.totals.innerHTML = display.join("<br>") } function set_size(width = 16, height = 16) { // call from console // even when we don't have rules, the non-match summaries give us the rule info // e.g. run_check(500000) (or a million) if (height > 16 || height < 2 || width > 16 || width < 2 ) { console.log("try again: width and height: max value 16, min value 2") return } else { console.log("setting sizes to "+ width +"w x "+ height +"h") } sizeW = width sizeH = height oRunningTotal = {} runningTotal = 0 runningPass = 0 runningMatch = 0 } populate_rules() setTimeout(function() { Promise.all([ get_globals() ]).then(function(){ run_checks(100) }) }, 50) </script> </body> </html> ================================================ FILE: tests/canvasspoof.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>canvas spoof detection</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} .togC {display: none;} .wordwrap {word-break: break-all; padding-right: 30px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#canvas">return to TZP index</a></td></tr> </table> <table id="tb9"> <col width="20%"><col width="20%"><col width="60%"> <thead><tr><th colspan="3"> <div class="nav-title">canvas spoof detection <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="3" class="intro"> <span class="no_color">Sets canvas with known results and reads them back.<p>FF102+ only: <span class="s9"><b> &#x2713; </b></span> means a match and a <span class="bad"><b> &#x2715; </b></span> means you lied </span> </td></tr> <tr> <td colspan="2" class="padr"><div class="btn-left"><span class="btn9 btn" onClick="run()">[ re-run ]</span></div>control</td> <td><canvas id="control1" width="16" height="16" style="border:1px solid white;"></canvas></td> </tr> <tr><td colspan="3"></td></tr> <!-- spacer --> <tr><td colspan="2" class="padr">getImageData</td><td class="c mono" id="getImageData"></td></tr> <tr><td colspan="2" class="padr">toDataURL</td><td class="c mono" id="toDataURL"></td></tr> <tr><td colspan="2" class="padr">toBlob</td><td class="c mono" id="toBlob"></td></tr> <tr><td colspan="3"></td></tr> <!-- spacer --> <tr><td colspan="2"></td><td class="s9">------</td></tr> <!-- spacer --> <tr><td colspan="3"></td></tr> <!-- spacer --> <tr><td colspan="2" class="padr">control</td><td> <canvas id="control2" width="16" height="16" style="border:1px solid white;"></canvas> </td> </tr> <tr><td colspan="2" class="padr">isPointInPath</td><td class="c mono" id="isPointInPath"></td></tr> <tr><td colspan="2" class="padr">isPointInStroke</td><td class="c mono" id="isPointInStroke"></td></tr> <tr><td colspan="3"></td></tr> <!-- spacer --> <tr><td colspan="3" class="showhide"> <span id="labelC" class="btnb" onClick="togglerows('C','base64 details')">&#9660; show base64 details</span></td></tr> <tr class="togC"><td id="labelDataURL" class="c padr">toDataURL</td><td colspan="2" class="c mono wordwrap" id="rawDataURL"></td></tr> <tr class="togC"><td id="labelBlob" class="c padr">toBlob</td><td colspan="2" class="c mono wordwrap" id="rawBlob"></td></tr> </table> <br> <script> 'use strict'; let canvasTDU = dom.control1, base64DTU // //https://stackoverflow.com/questions/37992117/how-to-get-image-color-mode-cmyk-rgb-in-javascript // ^ can I use this to rea the chunks and remove comment fields function test(b64 = '', sliceSize = 512) { // https://stackoverflow.com/questions/16968945/convert-base64-png-data-to-javascript-file-objects // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript /* the 'file' variable gives me insight into hexidecimal? e.g. PNG ... IEND https://en.wikipedia.org/wiki/PNG#File_format */ if ('' == b64) { b64 = base64DTU if (undefined == base64DTU) {return} } try { //foo++ let png = b64.split(',')[1] console.log('input\n-----\n', png) const byteCharacters = window.atob(png) const byteArrays = [] for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { const slice = byteCharacters.slice(offset, offset + sliceSize) const byteNumbers = new Array(slice.length) for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i) } const byteArray = new Uint8Array(byteNumbers) byteArrays.push(byteArray) } console.log('byteArrays\n-----\n', byteArrays) // this doesn't tell me anything as they always match let file = new Blob([window.atob(png)], {type: 'image/png', encoding: 'utf-8'}) console.log('file\n-----\n',file) let fr = new FileReader() fr.onload = function (oFREvent) { try { //foo++ var v = oFREvent.target.result.split(',')[1]; // encoding is messed up here, so we fix it v = atob(v) console.log('atob\n-----\n',v) let good_b64 = btoa(decodeURIComponent(escape(v))) console.log('match', good_b64 == png) //'data:image/png;base64,'+ good_b64 } catch(e) { console.log(e) } } fr.readAsDataURL(file) } catch(e) { console.log(e) } } function analyse(data, type) { if ('DataURL' == type) {base64DTU = data} let len = data.length, slice1 = data.slice(69,70), slice2 = data.slice(70,72), slice3 = data.slice(72,80), slice4 = data.slice(data.length - 10, data.length) let element = document.getElementById("raw"+ type) document.getElementById("label"+ type).innerHTML = "to"+ type if (slice1 == "A" && len > 161 && len < 191) { if (slice3 == "lEQVQ4jW" || slice3 == "lEQVQ4T2") { if (slice4 == "5ErkJggg==" || slice4 == "VORK5CYII=" || slice4 == "lFTkSuQmCC") { element.innerHTML = data.slice(0,69) + s14 + slice1 + sc + slice2 + s14 + slice3 + sc + data.slice(80, len - 10) + s14 + slice4 + sc + s9 +" ["+ len +"]"+ sc return } } } element.innerHTML = data + s9 +" ["+ len +"]"+ sc } function run() { // clear let items = document.getElementsByClassName("c") for(let i=0; i < items.length; i++) { items[i].innerHTML = "&nbsp" } // vars let oKnown = { // also FF137nightly: 1910796: Enable libz-rs on nightly: this changes our known hashes getImageData: [ 'c7f2099a', ], toDataURL: [ 'a8d0bd06', 'ef03b7d0', // 137+ libz-rs ], toBlob: [ 'a8d0bd06', 'ef03b7d0', // 137+ libz-rs ], isPointInPath: [ '2f4eafe2', ], isPointInStroke: [ '8722c710', ], } let oSuccess = { getImageData: false, toDataURL: false, toBlob: false, isPointInPath: false, isPointInStroke: false, } let sizeW = 16, sizeH = 8 dom.control1.width = sizeW dom.control1.height = sizeH dom.control2.width = sizeW dom.control2.height = sizeH var known = { createHashes: function(window){ let outputs = [ { name: "toDataURL", value: function(){ let data = getKnown().canvas.toDataURL() let hash = mini(data) analyse(data, "DataURL") oSuccess["toDataURL"] = true return hash } }, { name: "toBlob", value: function(){ return new Promise(function(resolve, reject){ try { var timeout = window.setTimeout(function(){ reject("timeout") }, 750) getKnown().canvas.toBlob(function(blob){ window.clearTimeout(timeout) var reader = new FileReader() reader.onload = function(){ let data = reader.result let hash = mini(data) analyse(data, "Blob") oSuccess["toBlob"] = true resolve(hash) } reader.onerror = function(){ reject("error") } reader.readAsDataURL(blob) }) } catch (e){ resolve(e.name) } }) } }, { class: window.CanvasRenderingContext2D, name: "getImageData", value: function(){ var context = getKnown() let imageData = context.getImageData(0,0,sizeW, sizeH) let data = mini(imageData.data) oSuccess["getImageData"] = true return data } }, { class: window.CanvasRenderingContext2D, name: "isPointInPath", value: function(){ let context2 = getKnownPath() let pathData = [] for (let x = 0; x < sizeW; x++){ for (let y = 0; y < sizeH; y++){ pathData.push(context2.isPointInPath(x, y)) } } oSuccess["isPointInPath"] = true return mini(pathData.join()) } }, { class: window.CanvasRenderingContext2D, name: "isPointInStroke", value: function(){ let context2 = getKnownPath() let pathStroke = [] for (let x = 0; x < sizeW; x++){ for (let y = 0; y < sizeH; y++){ pathStroke.push(context2.isPointInStroke(x, y)) } } oSuccess["isPointInStroke"] = true return mini(pathStroke.join()) } }, ]; function isSupported(output){ return !!(output.class? output.class: window.HTMLCanvasElement).prototype[output.name] } function getKnown(){ let canvas = document.getElementById("control1") let ctx = canvas.getContext('2d') //let aPixels = [] for (let x=0; x < sizeW; x++) { for (let y=0; y < sizeH; y++) { //aPixels.push("rgba(" + (x*y) +","+ (x*16) +","+ (y*16) +",255)") ctx.fillStyle = "rgba(" + (x*y) +","+ (x*16) +","+ (y*16) +",255)" ctx.fillRect(x, y, 1, 1) } } //console.log(aPixels) return ctx } function getKnownPath(){ let canvas2 = document.getElementById("control2") let ctx2 = canvas2.getContext('2d') ctx2.fillStyle = "rgba(255,255,255,255)" ctx2.beginPath() ctx2.rect(2,5,8,7) ctx2.closePath() ctx2.fill() return ctx2 } var finished = Promise.all(outputs.map(function(output){ return new Promise(function(resolve, reject){ var displayValue try { var supported = output.supported? output.supported(): isSupported(output); if (supported){ displayValue = output.value() } else { displayValue = "error" } } catch (e){ displayValue = "error" } Promise.resolve(displayValue).then(function(displayValue){ output.displayValue = displayValue resolve(output) }, function(e){ output.displayValue = e.name resolve(output) }) }) })) return finished } } // pause so users see change setTimeout(function(){ // vars let t0 = performance.now(), results = [] // get results Promise.all([ known.createHashes(window), ]).then(function(item){ item[0].forEach(function(data){ let name = data.name let value = data.displayValue if (oSuccess[name] == true) { if (isVer > 101) { value += (oKnown[name].includes(value) ? green_tick : red_cross) } } dom[name].innerHTML = value }) dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" }) }, 250) } Promise.all([ get_globals() ]).then(function(){ Promise.all([ get_isVer() ]).then(function(){ run() }) }) </script> </body> </html> ================================================ FILE: tests/chrome.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>chrome://</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 780px;} .togA, .togC {display: none;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#feature">return to TZP index</a></td></tr> </table> <!-- Tor Browser --> <table id="tb3"> <col width="20%"><col width="80%"> <thead><tr><th colspan="2">chrome:// & resource://</th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Enumeration of web accessible resources. <span class="s3">Firefox</span> list compiled from 128ESR + FF132. <span class="s3">Tor Browser</span> list compiled from 14.0 (128ESR). Click counts to display details. </span> </td></tr> <tr><td colspan="2"><span class="no_color">code based on work by <a target="_blank" class="blue" href="https://privacycheck.sec.lrz.de/active/fp_cd/fp_chrome_detect.html">privacycheck.sec.lrz.de</a>, updated lists thanks to <a target="_blank" class="blue" href="https://github.com/cypherpunks11">cypherpunks11</a></span> </td></tr> <tr><td colspan="2"><hr><br>TOR BROWSER</td></tr> <tr><td class="padr"><div> <div class="btn-left"><span class="btn3 btn" id="labelTor" onClick="run(`Tor`)">[ run ]</span></div> <div>images</div></div></td> <td class="c mono" id="imgHashTor"></td></tr> <tr><td class="padr">js</td><td class="c mono" id="jsHashTor"></td></tr> <tr><td class="padr">css</td><td class="c mono" id="cssHashTor"></td></tr> <tr><td class="padr">other</td><td class="c mono" id="otherHashTor"></td></tr> <tr><td class="padr">combined</td></td><td class="c mono" id="allHashTor"></td></tr> <tr><td colspan="2" class="showhide"><span id="labelA" class="btnb" onClick="togglerows('A')">&#9660; show details</span></td></tr> <tr class="togA"> <td class="padr"><span class="btn0 btn" onClick="copyclip(`detailTor`)">[ copy ]</span> results</td> <td class="c" id="detailTor"></td> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><hr><br>FIREFOX</td></tr> <tr><td class="padr"><div> <div class="btn-left"><span class="btn3 btn" id="labelFF" onClick="run(`FF`)">[ run ]</span></div> <div>images</div></div></td> <td class="c mono" id="imgHashFF"></td></tr> <tr><td class="padr">js</td><td class="c mono" id="jsHashFF"></td></tr> <tr><td class="padr">css</td><td class="c mono" id="cssHashFF"></td></tr> <tr><td class="padr">other</td><td class="c mono" id="otherHashFF"></td></tr> <tr><td class="padr">combined</td></td><td class="c mono" id="allHashFF"></td></tr> <tr><td colspan="2" class="showhide"><span id="labelC" class="btnb" onClick="togglerows('C')">&#9660; show details</span></td></tr> <tr class="togC"> <td class="padr"><span class="btn0 btn" onClick="copyclip(`detailFF`)">[ copy ]</span> results</td> <td class="c" id="detailFF"></td> </tr> </table> <br> <script> 'use strict'; function display(type, target) { let element = document.getElementById("detail"+ target) let array = sDetail[type + target] element.innerHTML = array.join("<br>") } function run(runtype) { // FF only if (!isFF) { return } // cosmetics document.getElementById("imgHash"+ runtype).innerHTML = "&nbsp" document.getElementById("allHash"+ runtype).innerHTML = "&nbsp" document.getElementById("otherHash"+ runtype).innerHTML = "&nbsp" document.getElementById("detail"+ runtype).innerHTML = "&nbsp" document.getElementById("jsHash"+ runtype).innerHTML = "tests are running" document.getElementById("cssHash"+ runtype).innerHTML = "please wait" // set list to use let list = runtype == "FF" ? listFF : listTor // build Uris let jsUris = list.match(/(chrome|resource):.+\.(js|jsm|mjs)$/gm).sort() let imgUris = list.match(/(chrome|resource):.+\.(svg|png|jpg|gif|ico|webp|avif)$/gm).sort() let cssUris = list.match(/(chrome|resource):.+\.css$/gm).sort() let otherUris = list.match(/(chrome|resource):.+\.(dtd|map|scss|eot|otf|ttf|woff|woff2|template|handlebars)$/gm)?.sort() ?? [] // VARS let allHash = [], jsHash = [], imgHash = [], cssHash = [], otherHash = [] // JS const get_js = () => new Promise(resolve => { let expected = jsUris.length, count = 0 jsUris.forEach(function(src) { let script = document.createElement('script') script.src = src document.head.appendChild(script) script.onload = function() { jsHash.push(src) allHash.push(src) count++ if (count == expected) {return resolve()} }; script.onerror = function() { count++ if (count == expected) {return resolve()} }; document.head.removeChild(script) }) }) // IMAGES const get_img = () => new Promise(resolve => { let expected = imgUris.length, count = 0 imgUris.forEach(function(imgUri) { let img = document.createElement("img"); img.src = imgUri img.style.height = "20px" img.style.width = "20px" img.onload = function() { imgHash.push(imgUri) allHash.push(imgUri) count++ if (count == expected) {return resolve()} } img.onerror = function() { count++ if (count == expected) {return resolve()} }; }) }) // CSS // FF121+: 1855861: we need to promise events let countCSS = 0, expectedCSS = 0 const get_eventCSS = (css, cssUri) => new Promise(resolve => { css.onload = function() { countCSS++ cssHash.push(cssUri) allHash.push(cssUri) document.head.removeChild(css) return resolve(countCSS) } css.onerror = function() { countCSS++ document.head.removeChild(css) return resolve(countCSS) } }) const get_css = () => new Promise(resolve => { expectedCSS = cssUris.length countCSS = 0 let count = 0 cssUris.forEach(function(cssUri) { let css = document.createElement("link") css.href = cssUri css.type = "text/css" css.rel = "stylesheet" document.head.appendChild(css, cssUri) // FF120+ use promises if (window.hasOwnProperty("UserActivation")) { Promise.all([ get_eventCSS(css, cssUri) ]).then(function(){ if (countCSS == expectedCSS) {return resolve()} }) } else { dom.cssHashTor.innerHTML = "i am groot" css.onload = function() { cssHash.push(cssUri) allHash.push(cssUri) count++ if (count == expectedCSS) {return resolve()} }; css.onerror = function() { count++ if (count == expectedCSS) {return resolve()} }; document.head.removeChild(css) } }) }) // OTHER let countOther = 0, expectedOther = 0 const get_eventOther = (css, otherUri) => new Promise(resolve => { css.onload = function() { countOther++ otherHash.push(otherUri) allHash.push(otherUri) document.head.removeChild(css) return resolve() } css.onerror = function() { countOther++ document.head.removeChild(css) return resolve() } }) const get_other = () => new Promise(resolve => { expectedOther = otherUris.length if (expectedOther == 0) { return resolve() } countOther = 0 let count = 0 otherUris.forEach(function(otherUri) { let other = document.createElement("link") other.href = otherUri other.rel = "stylesheet" document.head.appendChild(other) // FF120+ use promises if (window.hasOwnProperty("UserActivation")) { Promise.all([ get_eventOther(other, otherUri) ]).then(function(){ if (countOther == expectedOther) {return resolve()} }) } else { dom.otherHashTor.innerHTML = "i am groot" other.onload = function() { otherHash.push(otherUri) allHash.push(otherUri) count++ if (count == expectedOther) {return resolve()} }; other.onerror = function() { count++ if (count == expectedOther) {return resolve()} }; document.head.removeChild(other) } }) }) function output() { // CONSOLE let note = (runtype == "FF" ? "Firefox" : "Tor Browser") console.debug(note + " items", "\nIMG", imgUris, "\nJS", jsUris, "\nCSS", cssUris, "\nOTHER", otherUris, ) // sort imgHash.sort() jsHash.sort() cssHash.sort() otherHash.sort() allHash.sort() // counts let foundI = imgHash.length, foundJ = jsHash.length, foundC = cssHash.length, foundO = otherHash.length, foundA = allHash.length // hashes let hashI = mini(imgHash), hashJ = mini(jsHash), hashC = mini(cssHash), hashO = mini(otherHash), hashA = mini(allHash) // remember data sDetail["img"+ runtype] = imgHash sDetail["js"+ runtype] = jsHash sDetail["css"+ runtype] = cssHash sDetail["other"+ runtype] = otherHash // remember ordered all data let tmpArray = [] if (foundI) {tmpArray.push(s3 +"--- img ---"+ sc); tmpArray = tmpArray.concat(imgHash)} if (foundJ) {tmpArray.push(s3 +"--- js ---"+ sc); tmpArray = tmpArray.concat(jsHash)} if (foundC) {tmpArray.push(s3 +"--- css ---"+ sc); tmpArray = tmpArray.concat(cssHash)} if (foundO) {tmpArray.push(s3 +"--- other ---"+ sc); tmpArray = tmpArray.concat(otherHash)} sDetail["all"+ runtype] = tmpArray // item output let stats = " ["+ foundI +"/"+ imgUris.length +"]" if (foundI) {stats = " <span class='btn3 btnc' onclick='display(`img`,`"+ runtype +"`)'>" + stats +"</span>"} document.getElementById("imgHash"+ runtype).innerHTML = hashI + stats stats = " ["+ foundJ +"/"+ jsUris.length +"]" if (foundJ) {stats = " <span class='btn3 btnc' onclick='display(`js`,`"+ runtype +"`)'>" + stats +"</span>"} document.getElementById("jsHash"+ runtype).innerHTML = hashJ + stats stats = " ["+ foundC +"/"+ cssUris.length +"]" if (foundC) {stats = " <span class='btn3 btnc' onclick='display(`css`,`"+ runtype +"`)'>" + stats +"</span>"} document.getElementById("cssHash"+ runtype).innerHTML = hashC + stats stats = " ["+ foundO +"/"+ otherUris.length +"]" if (foundO) {stats = " <span class='btn3 btnc' onclick='display(`other`,`"+ runtype +"`)'>" + stats +"</span>"} document.getElementById("otherHash"+ runtype).innerHTML = hashO + stats let countTested = imgUris.length + jsUris.length + cssUris.length + otherUris.length stats = " ["+ foundA +"/"+ countTested +"]" if (foundA) {stats = " <span class='btn3 btnc' onclick='display(`all`,`"+ runtype +"`)'>" + stats +"</span>"} document.getElementById("allHash"+ runtype).innerHTML = mini(hashA) + stats // label document.getElementById("label" + runtype).innerHTML = "[ re-run ]" } Promise.all([ get_js(), get_img(), get_css(), get_other(), ]).then(function(results){ output() }) } get_globals() // listFF: removed: chrome://global/content/aboutServiceWorkers.js // this causes errors that prevents onClick function let listFF = ` chrome://activity-stream/content/css/activity-stream.css chrome://activity-stream/content/data/content/assets/confetti.svg chrome://activity-stream/content/data/content/assets/device-migration.svg chrome://activity-stream/content/data/content/assets/euo-chatbot.svg chrome://activity-stream/content/data/content/assets/euo-tab-orientation.svg chrome://activity-stream/content/data/content/assets/firefox.svg chrome://activity-stream/content/data/content/assets/fox-doodle-tail.png chrome://activity-stream/content/data/content/assets/fox-doodle-waving-static.png chrome://activity-stream/content/data/content/assets/fox-doodle-waving.gif chrome://activity-stream/content/data/content/assets/glyph-cfr-feature-16.svg chrome://activity-stream/content/data/content/assets/glyph-info-critical-16.svg chrome://activity-stream/content/data/content/assets/glyph-mail-16.svg chrome://activity-stream/content/data/content/assets/glyph-maximize-16.svg chrome://activity-stream/content/data/content/assets/glyph-minimize-16.svg chrome://activity-stream/content/data/content/assets/glyph-modal-delete-20.svg chrome://activity-stream/content/data/content/assets/glyph-newWindow-16.svg chrome://activity-stream/content/data/content/assets/glyph-open-file-16.svg chrome://activity-stream/content/data/content/assets/glyph-pin-16.svg chrome://activity-stream/content/data/content/assets/glyph-pocket-archive-16.svg chrome://activity-stream/content/data/content/assets/glyph-pocket-delete-16.svg chrome://activity-stream/content/data/content/assets/glyph-unpin-16.svg chrome://activity-stream/content/data/content/assets/glyph-webextension-16.svg chrome://activity-stream/content/data/content/assets/heart.webp chrome://activity-stream/content/data/content/assets/icon-removed-bookmark.svg chrome://activity-stream/content/data/content/assets/long-zap.svg chrome://activity-stream/content/data/content/assets/mobile-download-qr-existing-user-cn.svg chrome://activity-stream/content/data/content/assets/mobile-download-qr-existing-user.svg chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user-cn.svg chrome://activity-stream/content/data/content/assets/mobile-download-qr-new-user.svg chrome://activity-stream/content/data/content/assets/mr-amo-collection.svg chrome://activity-stream/content/data/content/assets/mr-gratitude.svg chrome://activity-stream/content/data/content/assets/mr-import.svg chrome://activity-stream/content/data/content/assets/mr-mobilecrosspromo.svg chrome://activity-stream/content/data/content/assets/mr-pinprivate.svg chrome://activity-stream/content/data/content/assets/mr-pintaskbar.svg chrome://activity-stream/content/data/content/assets/mr-privacysegmentation.svg chrome://activity-stream/content/data/content/assets/mr-rtamo-background-image.svg chrome://activity-stream/content/data/content/assets/mr-settodefault.svg chrome://activity-stream/content/data/content/assets/noodle-C.svg chrome://activity-stream/content/data/content/assets/noodle-outline-L.svg chrome://activity-stream/content/data/content/assets/noodle-solid-L.svg chrome://activity-stream/content/data/content/assets/nuo-taborientation.svg chrome://activity-stream/content/data/content/assets/person-typing.svg chrome://activity-stream/content/data/content/assets/pocket-onboarding.avif chrome://activity-stream/content/data/content/assets/pocket-onboarding@2x.avif chrome://activity-stream/content/data/content/assets/pocket-swoosh.svg chrome://activity-stream/content/data/content/assets/remote/mountain.svg chrome://activity-stream/content/data/content/assets/remote/umbrella.png chrome://activity-stream/content/data/content/assets/short-zap.svg chrome://activity-stream/content/data/content/assets/spinner.svg chrome://activity-stream/content/data/content/assets/sponsor-message-icon.svg chrome://activity-stream/content/data/content/assets/tabs-side-zap-transparent.svg chrome://activity-stream/content/data/content/assets/tabs-top-zap-transparent.svg chrome://activity-stream/content/data/content/assets/wallpapers/dark-beach.avif chrome://activity-stream/content/data/content/assets/wallpapers/dark-color.avif chrome://activity-stream/content/data/content/assets/wallpapers/dark-landscape.avif chrome://activity-stream/content/data/content/assets/wallpapers/dark-mountain.avif chrome://activity-stream/content/data/content/assets/wallpapers/dark-panda.avif chrome://activity-stream/content/data/content/assets/wallpapers/dark-sky.avif chrome://activity-stream/content/data/content/assets/wallpapers/light-beach.avif chrome://activity-stream/content/data/content/assets/wallpapers/light-color.avif chrome://activity-stream/content/data/content/assets/wallpapers/light-landscape.avif chrome://activity-stream/content/data/content/assets/wallpapers/light-mountain.avif chrome://activity-stream/content/data/content/assets/wallpapers/light-panda.avif chrome://activity-stream/content/data/content/assets/wallpapers/light-sky.avif chrome://activity-stream/content/data/content/tippytop/favicons/adidas.png chrome://activity-stream/content/data/content/tippytop/favicons/aliexpress-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/allegro-pl.ico chrome://activity-stream/content/data/content/tippytop/favicons/amazon.ico chrome://activity-stream/content/data/content/tippytop/favicons/avito-ru.ico chrome://activity-stream/content/data/content/tippytop/favicons/baidu-com.png chrome://activity-stream/content/data/content/tippytop/favicons/bbc-uk.ico chrome://activity-stream/content/data/content/tippytop/favicons/bing-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/ctrip-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/duckduckgo-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/ebay.ico chrome://activity-stream/content/data/content/tippytop/favicons/etsy.ico chrome://activity-stream/content/data/content/tippytop/favicons/facebook-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/geico.png chrome://activity-stream/content/data/content/tippytop/favicons/google-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/hrblock.ico chrome://activity-stream/content/data/content/tippytop/favicons/ifeng-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/iqiyi-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/leboncoin-fr.png chrome://activity-stream/content/data/content/tippytop/favicons/nike.ico chrome://activity-stream/content/data/content/tippytop/favicons/ok-ru.ico chrome://activity-stream/content/data/content/tippytop/favicons/olx-pl.ico chrome://activity-stream/content/data/content/tippytop/favicons/reddit-com.png chrome://activity-stream/content/data/content/tippytop/favicons/samsung.ico chrome://activity-stream/content/data/content/tippytop/favicons/turbotax.png chrome://activity-stream/content/data/content/tippytop/favicons/twitter-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/vk-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/vodafone.png chrome://activity-stream/content/data/content/tippytop/favicons/weibo-com.ico chrome://activity-stream/content/data/content/tippytop/favicons/wikipedia-org.ico chrome://activity-stream/content/data/content/tippytop/favicons/wix.ico chrome://activity-stream/content/data/content/tippytop/favicons/wykop-pl.png chrome://activity-stream/content/data/content/tippytop/favicons/yandex-com.png chrome://activity-stream/content/data/content/tippytop/favicons/yandex-ru.png chrome://activity-stream/content/data/content/tippytop/favicons/youtube-com.png chrome://activity-stream/content/data/content/tippytop/favicons/zhihu-com.ico chrome://activity-stream/content/data/content/tippytop/images/adidas@2x.png chrome://activity-stream/content/data/content/tippytop/images/aliexpress-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/allegro-pl@2x.png chrome://activity-stream/content/data/content/tippytop/images/amazon@2x.png chrome://activity-stream/content/data/content/tippytop/images/avito-ru@2x.png chrome://activity-stream/content/data/content/tippytop/images/baidu-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/bbc-uk@2x.png chrome://activity-stream/content/data/content/tippytop/images/bing-com@2x.svg chrome://activity-stream/content/data/content/tippytop/images/ctrip-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/duckduckgo-com@2x.svg chrome://activity-stream/content/data/content/tippytop/images/ebay@2x.png chrome://activity-stream/content/data/content/tippytop/images/etsy@2x.jpg chrome://activity-stream/content/data/content/tippytop/images/facebook-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/geico@2x.jpg chrome://activity-stream/content/data/content/tippytop/images/google-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/hrblock@2x.png chrome://activity-stream/content/data/content/tippytop/images/ifeng-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/iqiyi-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/leboncoin-fr@2x.png chrome://activity-stream/content/data/content/tippytop/images/nike@2x.jpg chrome://activity-stream/content/data/content/tippytop/images/ok-ru@2x.png chrome://activity-stream/content/data/content/tippytop/images/olx-pl@2x.png chrome://activity-stream/content/data/content/tippytop/images/reddit-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/samsung@2x.jpg chrome://activity-stream/content/data/content/tippytop/images/turbotax@2x.jpg chrome://activity-stream/content/data/content/tippytop/images/twitter-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/vk-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/vodafone@2x.jpg chrome://activity-stream/content/data/content/tippytop/images/weibo-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/wikipedia-org@2x.png chrome://activity-stream/content/data/content/tippytop/images/wix@2x.jpg chrome://activity-stream/content/data/content/tippytop/images/wykop-pl@2x.png chrome://activity-stream/content/data/content/tippytop/images/yandex-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/yandex-ru@2x.png chrome://activity-stream/content/data/content/tippytop/images/youtube-com@2x.png chrome://activity-stream/content/data/content/tippytop/images/zhihu-com@2x.png chrome://activity-stream/content/data/content/tippytop/top_sites.json chrome://branding/content/about-logo-private.png chrome://branding/content/about-logo-private@2x.png chrome://branding/content/about-logo.png chrome://branding/content/about-logo.svg chrome://branding/content/about-logo@2x.png chrome://branding/content/about-wordmark.svg chrome://branding/content/about.png chrome://branding/content/aboutDialog.css chrome://branding/content/document.ico chrome://branding/content/document_pdf.svg chrome://branding/content/favicon32.png chrome://branding/content/favicon64.png chrome://branding/content/firefox-wordmark.svg chrome://branding/content/icon128.png chrome://branding/content/icon16.png chrome://branding/content/icon32.png chrome://branding/content/icon48.png chrome://branding/content/icon64.png chrome://branding/locale/brand.properties chrome://browser/content/aboutDialog-appUpdater.js chrome://browser/content/aboutDialog.css chrome://browser/content/aboutDialog.js chrome://browser/content/aboutDialog.xhtml chrome://browser/content/aboutFrameCrashed.html chrome://browser/content/aboutPrivateBrowsing.css chrome://browser/content/aboutPrivateBrowsing.html chrome://browser/content/aboutPrivateBrowsing.js chrome://browser/content/aboutRestartRequired.js chrome://browser/content/aboutRestartRequired.xhtml chrome://browser/content/aboutRobots-icon.png chrome://browser/content/aboutRobots.css chrome://browser/content/aboutRobots.js chrome://browser/content/aboutRobots.xhtml chrome://browser/content/aboutSessionRestore.js chrome://browser/content/aboutSessionRestore.xhtml chrome://browser/content/aboutTabCrashed.css chrome://browser/content/aboutTabCrashed.js chrome://browser/content/aboutTabCrashed.xhtml chrome://browser/content/aboutWelcomeBack.xhtml chrome://browser/content/aboutlogins/aboutLogins.css chrome://browser/content/aboutlogins/aboutLogins.html chrome://browser/content/aboutlogins/aboutLogins.mjs chrome://browser/content/aboutlogins/aboutLoginsImportReport.css chrome://browser/content/aboutlogins/aboutLoginsImportReport.html chrome://browser/content/aboutlogins/aboutLoginsImportReport.mjs chrome://browser/content/aboutlogins/aboutLoginsUtils.mjs chrome://browser/content/aboutlogins/common.css chrome://browser/content/aboutlogins/components/confirmation-dialog.css chrome://browser/content/aboutlogins/components/confirmation-dialog.mjs chrome://browser/content/aboutlogins/components/fxaccounts-button.css chrome://browser/content/aboutlogins/components/fxaccounts-button.mjs chrome://browser/content/aboutlogins/components/generic-dialog.css chrome://browser/content/aboutlogins/components/generic-dialog.mjs chrome://browser/content/aboutlogins/components/import-details-row.mjs chrome://browser/content/aboutlogins/components/import-error-dialog.css chrome://browser/content/aboutlogins/components/import-error-dialog.mjs chrome://browser/content/aboutlogins/components/import-summary-dialog.css chrome://browser/content/aboutlogins/components/import-summary-dialog.mjs chrome://browser/content/aboutlogins/components/input-field/input-field.css chrome://browser/content/aboutlogins/components/input-field/input-field.mjs chrome://browser/content/aboutlogins/components/input-field/login-origin-field.mjs chrome://browser/content/aboutlogins/components/input-field/login-password-field.mjs chrome://browser/content/aboutlogins/components/input-field/login-username-field.mjs chrome://browser/content/aboutlogins/components/login-alert.css chrome://browser/content/aboutlogins/components/login-alert.mjs chrome://browser/content/aboutlogins/components/login-command-button.css chrome://browser/content/aboutlogins/components/login-command-button.mjs chrome://browser/content/aboutlogins/components/login-filter.css chrome://browser/content/aboutlogins/components/login-filter.mjs chrome://browser/content/aboutlogins/components/login-intro.css chrome://browser/content/aboutlogins/components/login-intro.mjs chrome://browser/content/aboutlogins/components/login-item.css chrome://browser/content/aboutlogins/components/login-item.mjs chrome://browser/content/aboutlogins/components/login-list-item.mjs chrome://browser/content/aboutlogins/components/login-list-lit-item.css chrome://browser/content/aboutlogins/components/login-list-lit-item.mjs chrome://browser/content/aboutlogins/components/login-list-section.mjs chrome://browser/content/aboutlogins/components/login-list.css chrome://browser/content/aboutlogins/components/login-list.mjs chrome://browser/content/aboutlogins/components/login-message-popup.css chrome://browser/content/aboutlogins/components/login-message-popup.mjs chrome://browser/content/aboutlogins/components/login-timeline.css chrome://browser/content/aboutlogins/components/login-timeline.mjs chrome://browser/content/aboutlogins/components/menu-button.css chrome://browser/content/aboutlogins/components/menu-button.mjs chrome://browser/content/aboutlogins/components/remove-logins-dialog.css chrome://browser/content/aboutlogins/components/remove-logins-dialog.mjs chrome://browser/content/aboutlogins/icons/breached-website.svg chrome://browser/content/aboutlogins/icons/intro-illustration.svg chrome://browser/content/aboutlogins/icons/password-hide.svg chrome://browser/content/aboutlogins/icons/password.svg chrome://browser/content/aboutlogins/icons/vulnerable-password.svg chrome://browser/content/aboutwelcome/aboutwelcome.bundle.js chrome://browser/content/aboutwelcome/aboutwelcome.css chrome://browser/content/aboutwelcome/aboutwelcome.html chrome://browser/content/asrouter/asrouter-admin.bundle.js chrome://browser/content/asrouter/asrouter-admin.html chrome://browser/content/asrouter/components/ASRouterAdmin/ASRouterAdmin.css chrome://browser/content/asrouter/components/remote-text.js chrome://browser/content/asrouter/render.js chrome://browser/content/asrouter/schemas/BackgroundTaskMessagingExperiment.schema.json chrome://browser/content/asrouter/schemas/MessagingExperiment.schema.json chrome://browser/content/assets/cookie-banners-begone.svg chrome://browser/content/assets/focus-logo.svg chrome://browser/content/assets/focus-promo.png chrome://browser/content/assets/focus-qr-code.svg chrome://browser/content/assets/klar-qr-code.svg chrome://browser/content/assets/moz-vpn.svg chrome://browser/content/assets/private-promo-asset.svg chrome://browser/content/assets/vpn-logo.svg chrome://browser/content/backup/ArchiveJSONBlock.1.schema.json chrome://browser/content/backup/BackupManifest.1.schema.json chrome://browser/content/backup/archive.css chrome://browser/content/backup/archive.js chrome://browser/content/backup/archive.template.html chrome://browser/content/backup/backup-common.css chrome://browser/content/backup/backup-constants.mjs chrome://browser/content/backup/backup-settings.css chrome://browser/content/backup/backup-settings.mjs chrome://browser/content/backup/disable-backup-encryption.css chrome://browser/content/backup/disable-backup-encryption.mjs chrome://browser/content/backup/enable-backup-encryption.css chrome://browser/content/backup/enable-backup-encryption.mjs chrome://browser/content/backup/password-rules-tooltip.css chrome://browser/content/backup/password-rules-tooltip.mjs chrome://browser/content/backup/password-validation-inputs.css chrome://browser/content/backup/password-validation-inputs.mjs chrome://browser/content/backup/restore-from-backup.css chrome://browser/content/backup/restore-from-backup.mjs chrome://browser/content/backup/turn-off-scheduled-backups.css chrome://browser/content/backup/turn-off-scheduled-backups.mjs chrome://browser/content/backup/turn-on-scheduled-backups.css chrome://browser/content/backup/turn-on-scheduled-backups.mjs chrome://browser/content/blanktab.html chrome://browser/content/blockedSite.js chrome://browser/content/blockedSite.xhtml chrome://browser/content/browser-a11yUtils.js chrome://browser/content/browser-addons.js chrome://browser/content/browser-captivePortal.js chrome://browser/content/browser-command-listeners.js chrome://browser/content/browser-commands.js chrome://browser/content/browser-context.js chrome://browser/content/browser-customization.js chrome://browser/content/browser-data-submission-info-bar.js chrome://browser/content/browser-fullScreenAndPointerLock.js chrome://browser/content/browser-gestureSupport.js chrome://browser/content/browser-graphics-utils.js chrome://browser/content/browser-init.js chrome://browser/content/browser-menubar.js chrome://browser/content/browser-pageActions.js chrome://browser/content/browser-pagestyle.js chrome://browser/content/browser-places.js chrome://browser/content/browser-profiles.js chrome://browser/content/browser-safebrowsing.js chrome://browser/content/browser-sets.js chrome://browser/content/browser-siteIdentity.js chrome://browser/content/browser-sitePermissionPanel.js chrome://browser/content/browser-siteProtections.js chrome://browser/content/browser-sync.js chrome://browser/content/browser-tabsintitlebar.js chrome://browser/content/browser-thumbnails.js chrome://browser/content/browser-toolbarKeyNav.js chrome://browser/content/browser-unified-extensions.js chrome://browser/content/browser-webrtc.js chrome://browser/content/browser.css chrome://browser/content/browser.js chrome://browser/content/browser.xhtml chrome://browser/content/built_in_addons.json chrome://browser/content/callout-tab-pickup-dark.svg chrome://browser/content/callout-tab-pickup.svg chrome://browser/content/child/ext-browser-content-only.js chrome://browser/content/child/ext-browser.js chrome://browser/content/child/ext-devtools-inspectedWindow.js chrome://browser/content/child/ext-devtools-network.js chrome://browser/content/child/ext-devtools-panels.js chrome://browser/content/child/ext-devtools.js chrome://browser/content/child/ext-menus-child.js chrome://browser/content/child/ext-menus.js chrome://browser/content/child/ext-omnibox.js chrome://browser/content/child/ext-tabs.js chrome://browser/content/contentSearchHandoffUI.js chrome://browser/content/contentSearchUI.css chrome://browser/content/contentSearchUI.js chrome://browser/content/contentTheme.js chrome://browser/content/customizableui/panelUI.js chrome://browser/content/default-bookmarks.html chrome://browser/content/downloads/allDownloadsView.js chrome://browser/content/downloads/contentAreaDownloadsView.css chrome://browser/content/downloads/contentAreaDownloadsView.js chrome://browser/content/downloads/contentAreaDownloadsView.xhtml chrome://browser/content/downloads/downloads.css chrome://browser/content/downloads/downloads.js chrome://browser/content/downloads/downloadsCommands.js chrome://browser/content/downloads/indicator.js chrome://browser/content/ext-browser.json chrome://browser/content/extension-popup-panel.css chrome://browser/content/extension.css chrome://browser/content/firefoxview/card-container.css chrome://browser/content/firefoxview/card-container.mjs chrome://browser/content/firefoxview/firefoxview.css chrome://browser/content/firefoxview/firefoxview.html chrome://browser/content/firefoxview/firefoxview.mjs chrome://browser/content/firefoxview/fxview-empty-state.css chrome://browser/content/firefoxview/fxview-empty-state.mjs chrome://browser/content/firefoxview/fxview-search-textbox.css chrome://browser/content/firefoxview/fxview-search-textbox.mjs chrome://browser/content/firefoxview/fxview-tab-list.css chrome://browser/content/firefoxview/fxview-tab-list.mjs chrome://browser/content/firefoxview/fxview-tab-row.css chrome://browser/content/firefoxview/helpers.mjs chrome://browser/content/firefoxview/history-empty.svg chrome://browser/content/firefoxview/history.css chrome://browser/content/firefoxview/history.mjs chrome://browser/content/firefoxview/opentabs-tab-list.css chrome://browser/content/firefoxview/opentabs-tab-list.mjs chrome://browser/content/firefoxview/opentabs-tab-row.css chrome://browser/content/firefoxview/opentabs.mjs chrome://browser/content/firefoxview/recentbrowsing.mjs chrome://browser/content/firefoxview/recentlyclosed-empty.svg chrome://browser/content/firefoxview/recentlyclosed.mjs chrome://browser/content/firefoxview/search-helpers.mjs chrome://browser/content/firefoxview/synced-tabs-error.svg chrome://browser/content/firefoxview/syncedtabs.mjs chrome://browser/content/firefoxview/view-history.svg chrome://browser/content/firefoxview/view-opentabs.css chrome://browser/content/firefoxview/view-opentabs.svg chrome://browser/content/firefoxview/view-recentbrowsing.svg chrome://browser/content/firefoxview/view-recentlyclosed.svg chrome://browser/content/firefoxview/view-syncedtabs.css chrome://browser/content/firefoxview/view-syncedtabs.svg chrome://browser/content/firefoxview/viewpage.mjs chrome://browser/content/genai/assets/brands/chatgpt.svg chrome://browser/content/genai/assets/brands/claude.svg chrome://browser/content/genai/assets/brands/gemini.svg chrome://browser/content/genai/assets/brands/huggingchat.svg chrome://browser/content/genai/assets/brands/lechat.svg chrome://browser/content/genai/assets/shortcuts-animated-dark.svg chrome://browser/content/genai/assets/shortcuts-animated.svg chrome://browser/content/genai/assets/shortcuts-static-dark.svg chrome://browser/content/genai/assets/shortcuts-static.svg chrome://browser/content/genai/chat.css chrome://browser/content/genai/chat.html chrome://browser/content/genai/chat.js chrome://browser/content/hiddenWindowMac.xhtml chrome://browser/content/ion.css chrome://browser/content/ion.html chrome://browser/content/ion.js chrome://browser/content/license.html chrome://browser/content/lockwise-card.mjs chrome://browser/content/logos/etp-mobile.svg chrome://browser/content/logos/fxa-logo.svg chrome://browser/content/logos/lockwise.svg chrome://browser/content/logos/monitor.svg chrome://browser/content/logos/passkey.svg chrome://browser/content/logos/proxy-dark.svg chrome://browser/content/logos/proxy-light.svg chrome://browser/content/logos/relay.svg chrome://browser/content/logos/send.svg chrome://browser/content/logos/tracking-protection-dark-theme.svg chrome://browser/content/logos/tracking-protection.svg chrome://browser/content/logos/vpn-dark.svg chrome://browser/content/logos/vpn-light.svg chrome://browser/content/logos/vpn-promo-logo.svg chrome://browser/content/main-popupset.js chrome://browser/content/messagepreview/limelight.svg chrome://browser/content/messagepreview/messagepreview.css chrome://browser/content/messagepreview/messagepreview.html chrome://browser/content/messagepreview/messagepreview.js chrome://browser/content/messagepreview/switch.svg chrome://browser/content/migration/brands/360.png chrome://browser/content/migration/brands/brave.png chrome://browser/content/migration/brands/canary.png chrome://browser/content/migration/brands/chrome.png chrome://browser/content/migration/brands/chromium.png chrome://browser/content/migration/brands/edge.png chrome://browser/content/migration/brands/edgebeta.png chrome://browser/content/migration/brands/ie.png chrome://browser/content/migration/brands/opera.png chrome://browser/content/migration/brands/operagx.png chrome://browser/content/migration/brands/safari.png chrome://browser/content/migration/brands/vivaldi.png chrome://browser/content/migration/migration-dialog-window.html chrome://browser/content/migration/migration-dialog-window.js chrome://browser/content/migration/migration-wizard-constants.mjs chrome://browser/content/migration/migration-wizard.mjs chrome://browser/content/monitor-card.mjs chrome://browser/content/nonbrowser-mac.js chrome://browser/content/nsContextMenu.js chrome://browser/content/nsContextMenu.sys.mjs chrome://browser/content/pagedata/schemas/article.schema.json chrome://browser/content/pagedata/schemas/audio.schema.json chrome://browser/content/pagedata/schemas/document.schema.json chrome://browser/content/pagedata/schemas/general.schema.json chrome://browser/content/pagedata/schemas/product.schema.json chrome://browser/content/pagedata/schemas/video.schema.json chrome://browser/content/pageinfo/pageInfo.css chrome://browser/content/pageinfo/pageInfo.js chrome://browser/content/pageinfo/pageInfo.xhtml chrome://browser/content/pageinfo/permissions.js chrome://browser/content/pageinfo/security.js chrome://browser/content/parent/ext-bookmarks.js chrome://browser/content/parent/ext-browser.js chrome://browser/content/parent/ext-browserAction.js chrome://browser/content/parent/ext-chrome-settings-overrides.js chrome://browser/content/parent/ext-commands.js chrome://browser/content/parent/ext-devtools-inspectedWindow.js chrome://browser/content/parent/ext-devtools-network.js chrome://browser/content/parent/ext-devtools-panels.js chrome://browser/content/parent/ext-devtools.js chrome://browser/content/parent/ext-find.js chrome://browser/content/parent/ext-history.js chrome://browser/content/parent/ext-menus.js chrome://browser/content/parent/ext-normandyAddonStudy.js chrome://browser/content/parent/ext-omnibox.js chrome://browser/content/parent/ext-pageAction.js chrome://browser/content/parent/ext-pkcs11.js chrome://browser/content/parent/ext-search.js chrome://browser/content/parent/ext-sessions.js chrome://browser/content/parent/ext-sidebarAction.js chrome://browser/content/parent/ext-tabs.js chrome://browser/content/parent/ext-topSites.js chrome://browser/content/parent/ext-url-overrides.js chrome://browser/content/parent/ext-windows.js chrome://browser/content/places/bookmarkProperties.js chrome://browser/content/places/bookmarkProperties.xhtml chrome://browser/content/places/bookmarksSidebar.js chrome://browser/content/places/bookmarksSidebar.xhtml chrome://browser/content/places/browserPlacesViews.js chrome://browser/content/places/controller.js chrome://browser/content/places/editBookmark.js chrome://browser/content/places/historySidebar.js chrome://browser/content/places/historySidebar.xhtml chrome://browser/content/places/places-commands.js chrome://browser/content/places/places-menupopup.js chrome://browser/content/places/places-tree.js chrome://browser/content/places/places.css chrome://browser/content/places/places.js chrome://browser/content/places/places.xhtml chrome://browser/content/places/treeView.js chrome://browser/content/policies/aboutPolicies.css chrome://browser/content/policies/aboutPolicies.html chrome://browser/content/policies/aboutPolicies.js chrome://browser/content/policies/policies-active.svg chrome://browser/content/policies/policies-documentation.svg chrome://browser/content/policies/policies-error.svg chrome://browser/content/preferences/containers.js chrome://browser/content/preferences/dialogs/addEngine.css chrome://browser/content/preferences/dialogs/addEngine.js chrome://browser/content/preferences/dialogs/addEngine.xhtml chrome://browser/content/preferences/dialogs/applicationManager.js chrome://browser/content/preferences/dialogs/applicationManager.xhtml chrome://browser/content/preferences/dialogs/blocklists.js chrome://browser/content/preferences/dialogs/blocklists.xhtml chrome://browser/content/preferences/dialogs/browserLanguages.js chrome://browser/content/preferences/dialogs/browserLanguages.xhtml chrome://browser/content/preferences/dialogs/clearSiteData.css chrome://browser/content/preferences/dialogs/clearSiteData.js chrome://browser/content/preferences/dialogs/clearSiteData.xhtml chrome://browser/content/preferences/dialogs/colors.js chrome://browser/content/preferences/dialogs/colors.xhtml chrome://browser/content/preferences/dialogs/connection.js chrome://browser/content/preferences/dialogs/connection.xhtml chrome://browser/content/preferences/dialogs/containers.js chrome://browser/content/preferences/dialogs/containers.xhtml chrome://browser/content/preferences/dialogs/dohExceptions.js chrome://browser/content/preferences/dialogs/dohExceptions.xhtml chrome://browser/content/preferences/dialogs/fonts.js chrome://browser/content/preferences/dialogs/fonts.xhtml chrome://browser/content/preferences/dialogs/handlers.css chrome://browser/content/preferences/dialogs/languages.js chrome://browser/content/preferences/dialogs/languages.xhtml chrome://browser/content/preferences/dialogs/permissions.js chrome://browser/content/preferences/dialogs/permissions.xhtml chrome://browser/content/preferences/dialogs/sanitize.js chrome://browser/content/preferences/dialogs/sanitize.xhtml chrome://browser/content/preferences/dialogs/selectBookmark.js chrome://browser/content/preferences/dialogs/selectBookmark.xhtml chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.js chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.xhtml chrome://browser/content/preferences/dialogs/siteDataSettings.js chrome://browser/content/preferences/dialogs/siteDataSettings.xhtml chrome://browser/content/preferences/dialogs/sitePermissions.css chrome://browser/content/preferences/dialogs/sitePermissions.js chrome://browser/content/preferences/dialogs/sitePermissions.xhtml chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml chrome://browser/content/preferences/dialogs/translationExceptions.js chrome://browser/content/preferences/dialogs/translationExceptions.xhtml chrome://browser/content/preferences/dialogs/translations.js chrome://browser/content/preferences/dialogs/translations.xhtml chrome://browser/content/preferences/experimental.js chrome://browser/content/preferences/extensionControlled.js chrome://browser/content/preferences/findInPage.js chrome://browser/content/preferences/fxaPairDevice.js chrome://browser/content/preferences/fxaPairDevice.xhtml chrome://browser/content/preferences/home.js chrome://browser/content/preferences/main.js chrome://browser/content/preferences/more-from-mozilla-qr-code-simple-cn.svg chrome://browser/content/preferences/more-from-mozilla-qr-code-simple.svg chrome://browser/content/preferences/moreFromMozilla.js chrome://browser/content/preferences/preferences.js chrome://browser/content/preferences/preferences.xhtml chrome://browser/content/preferences/privacy.js chrome://browser/content/preferences/search.js chrome://browser/content/preferences/sync.js chrome://browser/content/preferences/translations.js chrome://browser/content/preferences/web-appearance-dark.svg chrome://browser/content/preferences/web-appearance-light.svg chrome://browser/content/protections.css chrome://browser/content/protections.html chrome://browser/content/protections.mjs chrome://browser/content/proxy-card.mjs chrome://browser/content/robot.ico chrome://browser/content/safeMode.css chrome://browser/content/safeMode.js chrome://browser/content/safeMode.xhtml chrome://browser/content/sanitize.xhtml chrome://browser/content/sanitizeDialog.css chrome://browser/content/sanitizeDialog.js chrome://browser/content/sanitize_v2.xhtml chrome://browser/content/schemas/bookmarks.json chrome://browser/content/schemas/chrome_settings_overrides.json chrome://browser/content/schemas/commands.json chrome://browser/content/schemas/devtools.json chrome://browser/content/schemas/devtools_inspected_window.json chrome://browser/content/schemas/devtools_network.json chrome://browser/content/schemas/devtools_panels.json chrome://browser/content/schemas/find.json chrome://browser/content/schemas/history.json chrome://browser/content/schemas/menus.json chrome://browser/content/schemas/menus_child.json chrome://browser/content/schemas/normandyAddonStudy.json chrome://browser/content/schemas/omnibox.json chrome://browser/content/schemas/pkcs11.json chrome://browser/content/schemas/search.json chrome://browser/content/schemas/sessions.json chrome://browser/content/schemas/sidebar_action.json chrome://browser/content/schemas/tabs.json chrome://browser/content/schemas/top_sites.json chrome://browser/content/schemas/url_overrides.json chrome://browser/content/schemas/windows.json chrome://browser/content/screenshots/cancel.svg chrome://browser/content/screenshots/copied-notification.svg chrome://browser/content/screenshots/copy.svg chrome://browser/content/screenshots/download-white.svg chrome://browser/content/screenshots/download.svg chrome://browser/content/screenshots/fileHelpers.mjs chrome://browser/content/screenshots/icon-welcome-face-without-eyes.svg chrome://browser/content/screenshots/menu-fullpage.svg chrome://browser/content/screenshots/menu-visible.svg chrome://browser/content/screenshots/overlay/overlay.css chrome://browser/content/screenshots/overlayHelpers.mjs chrome://browser/content/screenshots/screenshots-buttons.css chrome://browser/content/screenshots/screenshots-buttons.js chrome://browser/content/screenshots/screenshots-preview.css chrome://browser/content/screenshots/screenshots-preview.html chrome://browser/content/screenshots/screenshots-preview.mjs chrome://browser/content/search/autocomplete-popup.js chrome://browser/content/search/searchbar.js chrome://browser/content/setDesktopBackground.js chrome://browser/content/setDesktopBackground.xhtml chrome://browser/content/shopping/adjusted-rating.mjs chrome://browser/content/shopping/analysis-explainer.css chrome://browser/content/shopping/analysis-explainer.mjs chrome://browser/content/shopping/assets/competitiveness.svg chrome://browser/content/shopping/assets/optInDark.avif chrome://browser/content/shopping/assets/optInLight.avif chrome://browser/content/shopping/assets/packaging.svg chrome://browser/content/shopping/assets/price.svg chrome://browser/content/shopping/assets/priceTagButtonCallout.svg chrome://browser/content/shopping/assets/quality.svg chrome://browser/content/shopping/assets/ratingDark.avif chrome://browser/content/shopping/assets/ratingLight.avif chrome://browser/content/shopping/assets/reviewsVisualCallout.svg chrome://browser/content/shopping/assets/shipping.svg chrome://browser/content/shopping/assets/shopping.svg chrome://browser/content/shopping/assets/unanalyzedDark.avif chrome://browser/content/shopping/assets/unanalyzedLight.avif chrome://browser/content/shopping/highlight-item.css chrome://browser/content/shopping/highlight-item.mjs chrome://browser/content/shopping/highlights.mjs chrome://browser/content/shopping/letter-grade.css chrome://browser/content/shopping/letter-grade.mjs chrome://browser/content/shopping/onboarding.mjs chrome://browser/content/shopping/recommended-ad.css chrome://browser/content/shopping/recommended-ad.mjs chrome://browser/content/shopping/reliability.mjs chrome://browser/content/shopping/settings.css chrome://browser/content/shopping/settings.mjs chrome://browser/content/shopping/shopping-card.css chrome://browser/content/shopping/shopping-card.mjs chrome://browser/content/shopping/shopping-container.css chrome://browser/content/shopping/shopping-container.mjs chrome://browser/content/shopping/shopping-message-bar.css chrome://browser/content/shopping/shopping-message-bar.mjs chrome://browser/content/shopping/shopping-page.css chrome://browser/content/shopping/shopping-sidebar.js chrome://browser/content/shopping/shopping.html chrome://browser/content/shopping/unanalyzed.css chrome://browser/content/shopping/unanalyzed.mjs chrome://browser/content/sidebar/browser-sidebar.js chrome://browser/content/sidebar/sidebar-customize.css chrome://browser/content/sidebar/sidebar-customize.html chrome://browser/content/sidebar/sidebar-customize.mjs chrome://browser/content/sidebar/sidebar-history.css chrome://browser/content/sidebar/sidebar-history.html chrome://browser/content/sidebar/sidebar-history.mjs chrome://browser/content/sidebar/sidebar-main.css chrome://browser/content/sidebar/sidebar-main.mjs chrome://browser/content/sidebar/sidebar-page.mjs chrome://browser/content/sidebar/sidebar-panel-header.css chrome://browser/content/sidebar/sidebar-panel-header.mjs chrome://browser/content/sidebar/sidebar-syncedtabs.html chrome://browser/content/sidebar/sidebar-syncedtabs.mjs chrome://browser/content/sidebar/sidebar-tab-list.css chrome://browser/content/sidebar/sidebar-tab-list.mjs chrome://browser/content/sidebar/sidebar-tab-row.css chrome://browser/content/sidebar/sidebar.css chrome://browser/content/spotlight.html chrome://browser/content/spotlight.js chrome://browser/content/static-robot.png chrome://browser/content/syncedtabs/sidebar.js chrome://browser/content/syncedtabs/sidebar.xhtml chrome://browser/content/tabbrowser/browser-allTabsMenu.js chrome://browser/content/tabbrowser/browser-ctrlTab.js chrome://browser/content/tabbrowser/browser-fullZoom.js chrome://browser/content/tabbrowser/tab-hover-preview.mjs chrome://browser/content/tabbrowser/tab.js chrome://browser/content/tabbrowser/tabbrowser.js chrome://browser/content/tabbrowser/tabgroup.js chrome://browser/content/tabbrowser/tabs.js chrome://browser/content/tabunloader/aboutUnloads.css chrome://browser/content/tabunloader/aboutUnloads.html chrome://browser/content/tabunloader/aboutUnloads.js chrome://browser/content/textrecognition/textrecognition.css chrome://browser/content/textrecognition/textrecognition.html chrome://browser/content/textrecognition/textrecognition.mjs chrome://browser/content/translations/TranslationsPanelShared.sys.mjs chrome://browser/content/translations/fullPageTranslationsPanel.js chrome://browser/content/translations/selectTranslationsPanel.js chrome://browser/content/urlbar/quicksuggestOnboarding.css chrome://browser/content/urlbar/quicksuggestOnboarding.html chrome://browser/content/urlbar/quicksuggestOnboarding.js chrome://browser/content/urlbar/quicksuggestOnboarding_magglass.svg chrome://browser/content/urlbar/quicksuggestOnboarding_magglass_animation.svg chrome://browser/content/urlbar/suggest-example.svg chrome://browser/content/usercontext/usercontext.css chrome://browser/content/utilityOverlay.js chrome://browser/content/vpn-card.mjs chrome://browser/content/webext-panels.js chrome://browser/content/webext-panels.xhtml chrome://browser/content/webrtcIndicator.js chrome://browser/content/webrtcIndicator.xhtml chrome://browser/locale/appstrings.properties chrome://browser/locale/browser.properties chrome://browser/locale/customizableui/customizableWidgets.properties chrome://browser/locale/downloads/downloads.properties chrome://browser/locale/feeds/subscribe.properties chrome://browser/locale/places/bookmarkProperties.properties chrome://browser/locale/safebrowsing/safebrowsing.properties chrome://browser/locale/search.properties chrome://browser/locale/shellservice.properties chrome://browser/locale/siteData.properties chrome://browser/locale/sitePermissions.properties chrome://browser/locale/syncSetup.properties chrome://browser/locale/taskbar.properties chrome://browser/locale/uiDensity.properties chrome://browser/skin/UITour.css chrome://browser/skin/aboutFrameCrashed.css chrome://browser/skin/aboutRestartRequired.css chrome://browser/skin/aboutSessionRestore.css chrome://browser/skin/aboutTabCrashed.css chrome://browser/skin/aboutWelcomeBack.css chrome://browser/skin/add-circle-fill.svg chrome://browser/skin/addon-notification.css chrome://browser/skin/addons/addon-install-blocked.svg chrome://browser/skin/addons/addon-install-downloading.svg chrome://browser/skin/addons/addon-install-installed.svg chrome://browser/skin/addons/extension-controlled.css chrome://browser/skin/addons/unified-extensions.css chrome://browser/skin/autocomplete.css chrome://browser/skin/back.svg chrome://browser/skin/blockedSite.css chrome://browser/skin/bookmark-12.svg chrome://browser/skin/bookmark-hollow.svg chrome://browser/skin/bookmark-star-on-tray.svg chrome://browser/skin/bookmark.svg chrome://browser/skin/bookmarks-toolbar.svg chrome://browser/skin/browser-colors.css chrome://browser/skin/browser-custom-colors.css chrome://browser/skin/browser-shared.css chrome://browser/skin/browser.css chrome://browser/skin/canvas-blocked.svg chrome://browser/skin/canvas.svg chrome://browser/skin/characterEncoding.svg chrome://browser/skin/chevron-animation.svg chrome://browser/skin/circle-check-dotted.svg chrome://browser/skin/contextmenu.css chrome://browser/skin/controlcenter/3rdpartycookies-blocked.svg chrome://browser/skin/controlcenter/3rdpartycookies.svg chrome://browser/skin/controlcenter/cryptominers.svg chrome://browser/skin/controlcenter/dashboard.svg chrome://browser/skin/controlcenter/etp-milestone.svg chrome://browser/skin/controlcenter/hero-message-background.svg chrome://browser/skin/controlcenter/mcb-disabled.svg chrome://browser/skin/controlcenter/panel.css chrome://browser/skin/controlcenter/tracking-protection.svg chrome://browser/skin/customizableui/customizeMode.css chrome://browser/skin/customizableui/density-compact.svg chrome://browser/skin/customizableui/density-normal.svg chrome://browser/skin/customizableui/density-touch.svg chrome://browser/skin/customizableui/empty-overflow-panel.png chrome://browser/skin/customizableui/empty-overflow-panel@2x.png chrome://browser/skin/customizableui/panelUI-shared.css chrome://browser/skin/customizableui/panelUI.css chrome://browser/skin/customizableui/whimsy.png chrome://browser/skin/customize.svg chrome://browser/skin/device-desktop.svg chrome://browser/skin/device-phone.svg chrome://browser/skin/device-tablet.svg chrome://browser/skin/device-tv.svg chrome://browser/skin/device-vr.svg chrome://browser/skin/downloads/allDownloadsView.css chrome://browser/skin/downloads/allDownloadsView.inc.css chrome://browser/skin/downloads/contentAreaDownloadsView.css chrome://browser/skin/downloads/download-blockedStates.css chrome://browser/skin/downloads/download-summary.svg chrome://browser/skin/downloads/downloads.css chrome://browser/skin/downloads/downloads.inc.css chrome://browser/skin/downloads/downloads.svg chrome://browser/skin/downloads/indicator.css chrome://browser/skin/downloads/notification-finish-animation.svg chrome://browser/skin/downloads/notification-start-animation.svg chrome://browser/skin/downloads/progressmeter.css chrome://browser/skin/drm-icon.svg chrome://browser/skin/edit-cut.svg chrome://browser/skin/edit-paste.svg chrome://browser/skin/fingerprint.svg chrome://browser/skin/firefox-view.svg chrome://browser/skin/flame.svg chrome://browser/skin/forget.svg chrome://browser/skin/formautofill-notification.css chrome://browser/skin/formautofill/icon-capture-address-fields.svg chrome://browser/skin/formautofill/icon-capture-email-fields.svg chrome://browser/skin/formautofill/icon-doorhanger-menu.svg chrome://browser/skin/forward.svg chrome://browser/skin/fullscreen-exit.svg chrome://browser/skin/fullscreen.svg chrome://browser/skin/fxa/avatar-color.svg chrome://browser/skin/fxa/avatar-empty.svg chrome://browser/skin/fxa/avatar.svg chrome://browser/skin/fxa/fxa-spinner.svg chrome://browser/skin/fxa/monitor.svg chrome://browser/skin/fxa/send-to-device.svg chrome://browser/skin/fxa/send.svg chrome://browser/skin/fxa/sync-devices.svg chrome://browser/skin/fxa/sync-illustration-issue.svg chrome://browser/skin/fxa/sync-illustration.svg chrome://browser/skin/history.svg chrome://browser/skin/home.svg chrome://browser/skin/identity-block/identity-block.css chrome://browser/skin/identity-credential-notification.css chrome://browser/skin/import-export.svg chrome://browser/skin/import.svg chrome://browser/skin/ion.svg chrome://browser/skin/library.svg chrome://browser/skin/light-dark-overrides.css chrome://browser/skin/login.svg chrome://browser/skin/logo-android.svg chrome://browser/skin/logo-ios.svg chrome://browser/skin/mail.svg chrome://browser/skin/menu-badged.svg chrome://browser/skin/menu.svg chrome://browser/skin/menupanel.css chrome://browser/skin/migration/migration-dialog-window.css chrome://browser/skin/migration/migration-wizard.css chrome://browser/skin/migration/progress-mask.svg chrome://browser/skin/migration/safari-icon-3dots.svg chrome://browser/skin/migration/success.svg chrome://browser/skin/monitor-base.png chrome://browser/skin/monitor-border.png chrome://browser/skin/new-tab.svg chrome://browser/skin/notification-fill-12.svg chrome://browser/skin/notification-icons.css chrome://browser/skin/notification-icons/autoplay-media-blocked.svg chrome://browser/skin/notification-icons/autoplay-media.svg chrome://browser/skin/notification-icons/camera-blocked.svg chrome://browser/skin/notification-icons/camera.png chrome://browser/skin/notification-icons/camera.svg chrome://browser/skin/notification-icons/desktop-notification-blocked.svg chrome://browser/skin/notification-icons/desktop-notification.svg chrome://browser/skin/notification-icons/drag-indicator.svg chrome://browser/skin/notification-icons/geo-blocked.svg chrome://browser/skin/notification-icons/geo.svg chrome://browser/skin/notification-icons/microphone-blocked.svg chrome://browser/skin/notification-icons/microphone.png chrome://browser/skin/notification-icons/microphone.svg chrome://browser/skin/notification-icons/midi.svg chrome://browser/skin/notification-icons/minimize.svg chrome://browser/skin/notification-icons/persistent-storage-blocked.svg chrome://browser/skin/notification-icons/persistent-storage.svg chrome://browser/skin/notification-icons/popup.svg chrome://browser/skin/notification-icons/screen-blocked.svg chrome://browser/skin/notification-icons/screen.png chrome://browser/skin/notification-icons/screen.svg chrome://browser/skin/notification-icons/speaker.svg chrome://browser/skin/notification-icons/xr-blocked.svg chrome://browser/skin/notification-icons/xr.svg chrome://browser/skin/open.svg chrome://browser/skin/pageInfo.css chrome://browser/skin/pageInfo.png chrome://browser/skin/panic-panel/header.png chrome://browser/skin/panic-panel/header@2x.png chrome://browser/skin/panic-panel/icons.png chrome://browser/skin/panic-panel/icons@2x.png chrome://browser/skin/permissions.svg chrome://browser/skin/pin-12.svg chrome://browser/skin/places/bookmarksMenu.svg chrome://browser/skin/places/bookmarksToolbar.svg chrome://browser/skin/places/editBookmark.css chrome://browser/skin/places/editBookmarkPanel.css chrome://browser/skin/places/folder-smart.svg chrome://browser/skin/places/organizer-shared.css chrome://browser/skin/places/organizer.css chrome://browser/skin/places/sidebar.css chrome://browser/skin/places/tag.svg chrome://browser/skin/places/tree-icons.css chrome://browser/skin/preferences/alwaysAsk.png chrome://browser/skin/preferences/android-menu.svg chrome://browser/skin/preferences/application.png chrome://browser/skin/preferences/applications.css chrome://browser/skin/preferences/category-general.svg chrome://browser/skin/preferences/category-privacy-security.svg chrome://browser/skin/preferences/category-search.svg chrome://browser/skin/preferences/category-sync.svg chrome://browser/skin/preferences/containers-dialog.css chrome://browser/skin/preferences/containers.css chrome://browser/skin/preferences/dialog.css chrome://browser/skin/preferences/fxaPairDevice.css chrome://browser/skin/preferences/ios-menu.svg chrome://browser/skin/preferences/monitor-logo.svg chrome://browser/skin/preferences/mozilla-logo.svg chrome://browser/skin/preferences/preferences.css chrome://browser/skin/preferences/privacy.css chrome://browser/skin/preferences/relay-logo.svg chrome://browser/skin/preferences/saveFile.png chrome://browser/skin/preferences/search-arrow-indicator.svg chrome://browser/skin/preferences/search.css chrome://browser/skin/preferences/siteDataSettings.css chrome://browser/skin/preferences/translations.css chrome://browser/skin/preferences/vpn-logo.svg chrome://browser/skin/privateBrowsing.svg chrome://browser/skin/privatebrowsing/aboutPrivateBrowsing.css chrome://browser/skin/privatebrowsing/favicon.svg chrome://browser/skin/profiler-popup-backdrop.png chrome://browser/skin/protections/breached-password.svg chrome://browser/skin/protections/new-feature.svg chrome://browser/skin/protections/resolved-breach-gray.svg chrome://browser/skin/protections/resolved-breach.svg chrome://browser/skin/quickactions.svg chrome://browser/skin/reader-mode.svg chrome://browser/skin/reload-to-stop.svg chrome://browser/skin/sanitizeDialog.css chrome://browser/skin/sanitizeDialog_v2.css chrome://browser/skin/save.svg chrome://browser/skin/screenshot.svg chrome://browser/skin/search-engine-placeholder.png chrome://browser/skin/search-engine-placeholder@2x.png chrome://browser/skin/search-indicator-badge-add.svg chrome://browser/skin/searchbar.css chrome://browser/skin/setDesktopBackground.css chrome://browser/skin/share.svg chrome://browser/skin/sidebar-collapsed.svg chrome://browser/skin/sidebar-expanded.svg chrome://browser/skin/sidebar-hidden.svg chrome://browser/skin/sidebar-horizontal-tabs.svg chrome://browser/skin/sidebar-right.svg chrome://browser/skin/sidebar.css chrome://browser/skin/sidebars-right.svg chrome://browser/skin/sidebars.svg chrome://browser/skin/sort.svg chrome://browser/skin/stop-to-reload.svg chrome://browser/skin/subtract-circle-fill.svg chrome://browser/skin/success-animation.svg chrome://browser/skin/sync.svg chrome://browser/skin/synced-tabs.svg chrome://browser/skin/syncedtabs/sidebar.css chrome://browser/skin/tab-crashed.svg chrome://browser/skin/tab-list-tree.css chrome://browser/skin/tab.svg chrome://browser/skin/tabbrowser/content-area.css chrome://browser/skin/tabbrowser/crashed.svg chrome://browser/skin/tabbrowser/ctrlTab.css chrome://browser/skin/tabbrowser/fullscreen-and-pointerlock.css chrome://browser/skin/tabbrowser/loading-burst.svg chrome://browser/skin/tabbrowser/loading.svg chrome://browser/skin/tabbrowser/pendingpaint.png chrome://browser/skin/tabbrowser/tab-audio-blocked-small.svg chrome://browser/skin/tabbrowser/tab-audio-muted-small.svg chrome://browser/skin/tabbrowser/tab-audio-playing-small.svg chrome://browser/skin/tabbrowser/tab-connecting.png chrome://browser/skin/tabbrowser/tab-connecting@2x.png chrome://browser/skin/tabbrowser/tab-drag-indicator.svg chrome://browser/skin/tabbrowser/tab-hover-preview.css chrome://browser/skin/tabbrowser/tab-loading-inverted.png chrome://browser/skin/tabbrowser/tab-loading-inverted@2x.png chrome://browser/skin/tabbrowser/tab-loading.png chrome://browser/skin/tabbrowser/tab-loading@2x.png chrome://browser/skin/tabbrowser/tabs.css chrome://browser/skin/thumb-down.svg chrome://browser/skin/toolbar-drag-indicator.svg chrome://browser/skin/toolbarbutton-icons.css chrome://browser/skin/toolbarbuttons.css chrome://browser/skin/topsites.svg chrome://browser/skin/tracking-protection-active.svg chrome://browser/skin/tracking-protection-disabled.svg chrome://browser/skin/tracking-protection.svg chrome://browser/skin/translations.svg chrome://browser/skin/translations/beta.svg chrome://browser/skin/translations/panel.css chrome://browser/skin/trending.svg chrome://browser/skin/update-badge.svg chrome://browser/skin/urlbar-dynamic-results.css chrome://browser/skin/urlbar-searchbar.css chrome://browser/skin/urlbarView.css chrome://browser/skin/weather/cloudy.svg chrome://browser/skin/weather/flurries.svg chrome://browser/skin/weather/fog.svg chrome://browser/skin/weather/freezing-rain.svg chrome://browser/skin/weather/hazy-sunshine.svg chrome://browser/skin/weather/hot.svg chrome://browser/skin/weather/ice.svg chrome://browser/skin/weather/mostly-cloudy-with-showers.svg chrome://browser/skin/weather/mostly-cloudy-with-thunderstorms.svg chrome://browser/skin/weather/mostly-sunny.svg chrome://browser/skin/weather/night-clear.svg chrome://browser/skin/weather/night-hazy-moonlight.svg chrome://browser/skin/weather/night-mostly-clear.svg chrome://browser/skin/weather/night-mostly-cloudy-with-flurries.svg chrome://browser/skin/weather/night-partly-cloudy-with-showers.svg chrome://browser/skin/weather/night-partly-cloudy-with-thunderstorms.svg chrome://browser/skin/weather/partly-sunny-with-flurries.svg chrome://browser/skin/weather/partly-sunny.svg chrome://browser/skin/weather/rain.svg chrome://browser/skin/weather/showers.svg chrome://browser/skin/weather/snow.svg chrome://browser/skin/weather/sunny.svg chrome://browser/skin/weather/thunderstorms.svg chrome://browser/skin/weather/windy.svg chrome://browser/skin/webRTC-indicator.css chrome://browser/skin/webRTC-menubar-indicator.css chrome://browser/skin/window-controls/close-highcontrast.svg chrome://browser/skin/window-controls/close-themes.svg chrome://browser/skin/window-controls/close.svg chrome://browser/skin/window-controls/maximize-highcontrast.svg chrome://browser/skin/window-controls/maximize-themes.svg chrome://browser/skin/window-controls/maximize.svg chrome://browser/skin/window-controls/minimize-highcontrast.svg chrome://browser/skin/window-controls/minimize-themes.svg chrome://browser/skin/window-controls/minimize.svg chrome://browser/skin/window-controls/restore-highcontrast.svg chrome://browser/skin/window-controls/restore-themes.svg chrome://browser/skin/window-controls/restore.svg chrome://browser/skin/window.svg chrome://devtools-jsonview-styles/content/general.css chrome://devtools-jsonview-styles/content/headers-panel.css chrome://devtools-jsonview-styles/content/json-panel.css chrome://devtools-jsonview-styles/content/main.css chrome://devtools-jsonview-styles/content/search-box.css chrome://devtools-jsonview-styles/content/text-panel.css chrome://devtools-jsonview-styles/content/toolbar.css chrome://global/content/TopLevelVideoDocument.js chrome://global/content/aboutAbout.html chrome://global/content/aboutAbout.js chrome://global/content/aboutCheckerboard.css chrome://global/content/aboutCheckerboard.html chrome://global/content/aboutCheckerboard.js chrome://global/content/aboutGlean.css chrome://global/content/aboutGlean.html chrome://global/content/aboutGlean.js chrome://global/content/aboutLogging.html chrome://global/content/aboutLogging.js chrome://global/content/aboutMemory.css chrome://global/content/aboutMemory.js chrome://global/content/aboutMemory.xhtml chrome://global/content/aboutMozilla.css chrome://global/content/aboutNetError.html chrome://global/content/aboutNetError.mjs chrome://global/content/aboutNetworking.html chrome://global/content/aboutNetworking.js chrome://global/content/aboutProcesses.css chrome://global/content/aboutProcesses.html chrome://global/content/aboutProcesses.js chrome://global/content/aboutProfiles.js chrome://global/content/aboutProfiles.xhtml chrome://global/content/aboutRights.js chrome://global/content/aboutRights.xhtml chrome://global/content/aboutServiceWorkers.xhtml chrome://global/content/aboutSupport.js chrome://global/content/aboutSupport.xhtml chrome://global/content/aboutTelemetry.css chrome://global/content/aboutTelemetry.js chrome://global/content/aboutTelemetry.xhtml chrome://global/content/aboutThirdParty.css chrome://global/content/aboutThirdParty.html chrome://global/content/aboutThirdParty.js chrome://global/content/aboutUrlClassifier.css chrome://global/content/aboutUrlClassifier.js chrome://global/content/aboutUrlClassifier.xhtml chrome://global/content/aboutWebauthn.css chrome://global/content/aboutWebauthn.html chrome://global/content/aboutWebauthn.js chrome://global/content/aboutWindowsMessages.css chrome://global/content/aboutWindowsMessages.html chrome://global/content/aboutWindowsMessages.js chrome://global/content/aboutconfig/aboutconfig.css chrome://global/content/aboutconfig/aboutconfig.html chrome://global/content/aboutconfig/aboutconfig.js chrome://global/content/aboutconfig/background.svg chrome://global/content/aboutconfig/toggle.svg chrome://global/content/aboutwebrtc/aboutWebrtc.css chrome://global/content/aboutwebrtc/aboutWebrtc.html chrome://global/content/aboutwebrtc/aboutWebrtc.mjs chrome://global/content/aboutwebrtc/configurationList.mjs chrome://global/content/aboutwebrtc/copyButton.mjs chrome://global/content/aboutwebrtc/disclosure.mjs chrome://global/content/aboutwebrtc/graph.mjs chrome://global/content/aboutwebrtc/graphdb.mjs chrome://global/content/adjustableTitle.js chrome://global/content/alerts/alert.css chrome://global/content/alerts/alert.js chrome://global/content/alerts/alert.xhtml chrome://global/content/antitracking/StripOnShare.json chrome://global/content/antitracking/StripOnShareLGPL.json chrome://global/content/appPicker.js chrome://global/content/appPicker.xhtml chrome://global/content/autocomplete.css chrome://global/content/backgroundPageThumbs.xhtml chrome://global/content/bindings/calendar.js chrome://global/content/bindings/datekeeper.js chrome://global/content/bindings/datepicker.js chrome://global/content/bindings/datetimebox.css chrome://global/content/bindings/spinner.js chrome://global/content/bindings/timekeeper.js chrome://global/content/bindings/timepicker.js chrome://global/content/buildconfig.css chrome://global/content/buildconfig.html chrome://global/content/certviewer/certDecoder.mjs chrome://global/content/certviewer/certviewer.css chrome://global/content/certviewer/certviewer.html chrome://global/content/certviewer/certviewer.mjs chrome://global/content/certviewer/components/about-certificate-items.mjs chrome://global/content/certviewer/components/about-certificate-section.css chrome://global/content/certviewer/components/about-certificate-section.mjs chrome://global/content/certviewer/components/certificate-section.css chrome://global/content/certviewer/components/certificate-section.mjs chrome://global/content/certviewer/components/certificate-tabs-section.mjs chrome://global/content/certviewer/components/error-section.css chrome://global/content/certviewer/components/error-section.mjs chrome://global/content/certviewer/components/info-group-container.mjs chrome://global/content/certviewer/components/info-group.css chrome://global/content/certviewer/components/info-group.mjs chrome://global/content/certviewer/components/info-item.css chrome://global/content/certviewer/components/info-item.mjs chrome://global/content/certviewer/components/list-item.css chrome://global/content/certviewer/components/list-item.mjs chrome://global/content/certviewer/components/utils.mjs chrome://global/content/certviewer/vendor/pkijs.js chrome://global/content/commonDialog.css chrome://global/content/commonDialog.js chrome://global/content/commonDialog.xhtml chrome://global/content/contentAreaUtils.js chrome://global/content/cookiebanners/CookieBannerRule.schema.json chrome://global/content/crashes.css chrome://global/content/crashes.html chrome://global/content/crashes.js chrome://global/content/customElements.js chrome://global/content/datepicker.xhtml chrome://global/content/defaultagent/fox-doodle-peek.png chrome://global/content/editMenuOverlay.js chrome://global/content/elements/arrowscrollbox.js chrome://global/content/elements/autocomplete-input.js chrome://global/content/elements/autocomplete-popup.js chrome://global/content/elements/autocomplete-richlistitem.js chrome://global/content/elements/browser-custom-element.js chrome://global/content/elements/button.js chrome://global/content/elements/checkbox.js chrome://global/content/elements/datetimebox.js chrome://global/content/elements/dialog.js chrome://global/content/elements/editor.js chrome://global/content/elements/findbar.js chrome://global/content/elements/general.js chrome://global/content/elements/infobar.css chrome://global/content/elements/marquee.css chrome://global/content/elements/marquee.js chrome://global/content/elements/menu.js chrome://global/content/elements/menulist.js chrome://global/content/elements/menupopup.js chrome://global/content/elements/moz-button-group.css chrome://global/content/elements/moz-button-group.mjs chrome://global/content/elements/moz-button.css chrome://global/content/elements/moz-button.mjs chrome://global/content/elements/moz-card.css chrome://global/content/elements/moz-card.mjs chrome://global/content/elements/moz-checkbox.css chrome://global/content/elements/moz-checkbox.mjs chrome://global/content/elements/moz-fieldset.css chrome://global/content/elements/moz-fieldset.mjs chrome://global/content/elements/moz-five-star.css chrome://global/content/elements/moz-five-star.mjs chrome://global/content/elements/moz-input-box.js chrome://global/content/elements/moz-label.css chrome://global/content/elements/moz-label.mjs chrome://global/content/elements/moz-message-bar.css chrome://global/content/elements/moz-message-bar.mjs chrome://global/content/elements/moz-page-nav-button.css chrome://global/content/elements/moz-page-nav.css chrome://global/content/elements/moz-page-nav.mjs chrome://global/content/elements/moz-radio-group.css chrome://global/content/elements/moz-radio-group.mjs chrome://global/content/elements/moz-radio.css chrome://global/content/elements/moz-support-link.mjs chrome://global/content/elements/moz-toggle.css chrome://global/content/elements/moz-toggle.mjs chrome://global/content/elements/named-deck.js chrome://global/content/elements/notificationbox.js chrome://global/content/elements/panel-item.css chrome://global/content/elements/panel-list.css chrome://global/content/elements/panel-list.js chrome://global/content/elements/panel.js chrome://global/content/elements/popupnotification.js chrome://global/content/elements/radio.js chrome://global/content/elements/richlistbox.js chrome://global/content/elements/search-textbox.js chrome://global/content/elements/stringbundle.js chrome://global/content/elements/tabbox.js chrome://global/content/elements/text.js chrome://global/content/elements/textrecognition.js chrome://global/content/elements/toolbarbutton.js chrome://global/content/elements/tree.js chrome://global/content/elements/videocontrols.js chrome://global/content/elements/wizard.js chrome://global/content/filepicker.properties chrome://global/content/globalOverlay.js chrome://global/content/gmp-sources/openh264.json chrome://global/content/gmp-sources/widevinecdm.json chrome://global/content/gmp-sources/widevinecdm_l1.json chrome://global/content/httpsonlyerror/errorpage.html chrome://global/content/httpsonlyerror/errorpage.js chrome://global/content/httpsonlyerror/secure-broken.svg chrome://global/content/license.html chrome://global/content/lit-utils.mjs chrome://global/content/macWindowMenu.js chrome://global/content/megalist/Dialog.mjs chrome://global/content/megalist/LoginFormComponent/login-form.css chrome://global/content/megalist/LoginLine.css chrome://global/content/megalist/LoginLine.mjs chrome://global/content/megalist/MegalistAlpha.mjs chrome://global/content/megalist/MegalistView.mjs chrome://global/content/megalist/NotificationMessageBar.mjs chrome://global/content/megalist/PasswordCard.css chrome://global/content/megalist/PasswordCard.mjs chrome://global/content/megalist/VirtualizedList.mjs chrome://global/content/megalist/megalist.css chrome://global/content/megalist/megalist.html chrome://global/content/ml/EngineProcess.sys.mjs chrome://global/content/ml/MLEngine.html chrome://global/content/ml/MLEngine.worker.mjs chrome://global/content/ml/ModelHub.sys.mjs chrome://global/content/ml/ONNXPipeline.mjs chrome://global/content/ml/Utils.sys.mjs chrome://global/content/ml/ort-wasm-simd-threaded.jsep.mjs chrome://global/content/ml/ort-wasm-simd-threaded.mjs chrome://global/content/ml/ort.mjs chrome://global/content/ml/ort.webgpu.mjs chrome://global/content/ml/transformers.js chrome://global/content/mozilla.html chrome://global/content/neterror/aboutNetErrorCodes.js chrome://global/content/neterror/supportpages/connection-not-secure.html chrome://global/content/neterror/supportpages/time-errors.html chrome://global/content/notfound.wav chrome://global/content/pictureinpicture/player.js chrome://global/content/pictureinpicture/player.xhtml chrome://global/content/preferencesBindings.js chrome://global/content/print.css chrome://global/content/print.html chrome://global/content/print.js chrome://global/content/printPageSetup.js chrome://global/content/printPageSetup.xhtml chrome://global/content/printPagination.css chrome://global/content/printPreview.css chrome://global/content/printPreviewPagination.js chrome://global/content/printUtils.js chrome://global/content/process-content.js chrome://global/content/reader/aboutReader.html chrome://global/content/reader/color-input.css chrome://global/content/reader/color-input.mjs chrome://global/content/reader/moz-slider.css chrome://global/content/reader/moz-slider.mjs chrome://global/content/resetProfile.css chrome://global/content/resetProfile.js chrome://global/content/resetProfile.xhtml chrome://global/content/resetProfileProgress.xhtml chrome://global/content/selectDialog.css chrome://global/content/selectDialog.js chrome://global/content/selectDialog.xhtml chrome://global/content/shopping/ProductConfig.mjs chrome://global/content/shopping/ProductValidator.sys.mjs chrome://global/content/shopping/ShoppingProduct.mjs chrome://global/content/shopping/analysis_request.schema.json chrome://global/content/shopping/analysis_response.schema.json chrome://global/content/shopping/analysis_status_request.schema.json chrome://global/content/shopping/analysis_status_response.schema.json chrome://global/content/shopping/analyze_request.schema.json chrome://global/content/shopping/analyze_response.schema.json chrome://global/content/shopping/attribution_request.schema.json chrome://global/content/shopping/attribution_response.schema.json chrome://global/content/shopping/recommendations_request.schema.json chrome://global/content/shopping/recommendations_response.schema.json chrome://global/content/shopping/reporting_request.schema.json chrome://global/content/shopping/reporting_response.schema.json chrome://global/content/simplifyMode.css chrome://global/content/third_party/cfworker/json-schema.js chrome://global/content/third_party/d3/d3.js chrome://global/content/timepicker.xhtml chrome://global/content/toggle-group.css chrome://global/content/translations/TranslationsTelemetry.sys.mjs chrome://global/content/translations/Translator.mjs chrome://global/content/translations/bergamot-translator.js chrome://global/content/translations/icons/swap-languages.svg chrome://global/content/translations/translations-document.sys.mjs chrome://global/content/translations/translations-engine.html chrome://global/content/translations/translations-engine.sys.mjs chrome://global/content/translations/translations-engine.worker.js chrome://global/content/translations/translations.css chrome://global/content/translations/translations.html chrome://global/content/translations/translations.mjs chrome://global/content/treeUtils.js chrome://global/content/usercharacteristics/gl-matrix.js chrome://global/content/usercharacteristics/ssdeep.js chrome://global/content/usercharacteristics/usercharacteristics.css chrome://global/content/usercharacteristics/usercharacteristics.html chrome://global/content/usercharacteristics/usercharacteristics.js chrome://global/content/vendor/lit.all.mjs chrome://global/content/viewSourceUtils.js chrome://global/content/viewZoomOverlay.js chrome://global/content/widgets.css chrome://global/content/win.xhtml chrome://global/content/xml/XMLPrettyPrint.css chrome://global/content/xml/XMLPrettyPrint.xsl chrome://global/content/xul.css chrome://global/locale/AccessFu.properties chrome://global/locale/aboutStudies.properties chrome://global/locale/appstrings.properties chrome://global/locale/autocomplete.properties chrome://global/locale/browser.properties chrome://global/locale/commonDialogs.properties chrome://global/locale/contentAreaCommands.properties chrome://global/locale/css.properties chrome://global/locale/dialog.properties chrome://global/locale/dom/dom.properties chrome://global/locale/extensions.properties chrome://global/locale/fallbackMenubar.properties chrome://global/locale/filepicker.properties chrome://global/locale/global-strres.properties chrome://global/locale/intl.css chrome://global/locale/intl.properties chrome://global/locale/keys.properties chrome://global/locale/layout/HtmlForm.properties chrome://global/locale/layout/MediaDocument.properties chrome://global/locale/layout/htmlparser.properties chrome://global/locale/layout/xmlparser.properties chrome://global/locale/layout_errors.properties chrome://global/locale/mathml/mathml.properties chrome://global/locale/narrate.properties chrome://global/locale/nsWebBrowserPersist.properties chrome://global/locale/printdialog.properties chrome://global/locale/printing.properties chrome://global/locale/resetProfile.properties chrome://global/locale/security/caps.properties chrome://global/locale/security/csp.properties chrome://global/locale/security/security.properties chrome://global/locale/svg/svg.properties chrome://global/locale/viewSource.properties chrome://global/locale/wizard.properties chrome://global/locale/xslt/xslt.properties chrome://global/locale/xul.properties chrome://global/skin/aboutCache.css chrome://global/skin/aboutCacheEntry.css chrome://global/skin/aboutHttpsOnlyError.css chrome://global/skin/aboutLicense.css chrome://global/skin/aboutLogging.css chrome://global/skin/aboutMemory.css chrome://global/skin/aboutNetError.css chrome://global/skin/aboutNetworking.css chrome://global/skin/aboutReader.css chrome://global/skin/aboutRights.css chrome://global/skin/aboutSupport.css chrome://global/skin/alert.css chrome://global/skin/appPicker.css chrome://global/skin/arrow/panelarrow-vertical.svg chrome://global/skin/arrowscrollbox.css chrome://global/skin/autocomplete.css chrome://global/skin/button.css chrome://global/skin/checkbox.css chrome://global/skin/close-icon.css chrome://global/skin/commonDialog.css chrome://global/skin/datetimeinputpickers.css chrome://global/skin/design-system/text-and-typography.css chrome://global/skin/design-system/tokens-brand.css chrome://global/skin/design-system/tokens-platform.css chrome://global/skin/design-system/tokens-shared.css chrome://global/skin/dialog.css chrome://global/skin/dirListing/dirListing.css chrome://global/skin/dirListing/folder.png chrome://global/skin/dirListing/up.png chrome://global/skin/error-pages.css chrome://global/skin/findbar.css chrome://global/skin/global-shared.css chrome://global/skin/global.css chrome://global/skin/icons/Authentication.png chrome://global/skin/icons/Landscape.png chrome://global/skin/icons/Portrait.png chrome://global/skin/icons/arrow-down-12.svg chrome://global/skin/icons/arrow-down.svg chrome://global/skin/icons/arrow-left-12.svg chrome://global/skin/icons/arrow-left.svg chrome://global/skin/icons/arrow-right-12.svg chrome://global/skin/icons/arrow-right.svg chrome://global/skin/icons/arrow-up-12.svg chrome://global/skin/icons/arrow-up.svg chrome://global/skin/icons/autoscroll-horizontal.svg chrome://global/skin/icons/autoscroll-vertical.svg chrome://global/skin/icons/autoscroll.svg chrome://global/skin/icons/badge-blue.svg chrome://global/skin/icons/blocked.svg chrome://global/skin/icons/check-filled.svg chrome://global/skin/icons/check-partial.svg chrome://global/skin/icons/check.svg chrome://global/skin/icons/chevron.svg chrome://global/skin/icons/clipboard.svg chrome://global/skin/icons/close-12.svg chrome://global/skin/icons/close-fill.svg chrome://global/skin/icons/close.svg chrome://global/skin/icons/columnpicker.svg chrome://global/skin/icons/content-analysis.svg chrome://global/skin/icons/defaultFavicon.svg chrome://global/skin/icons/delete.svg chrome://global/skin/icons/developer.svg chrome://global/skin/icons/edit-copy.svg chrome://global/skin/icons/edit-outline.svg chrome://global/skin/icons/edit.svg chrome://global/skin/icons/error-64.png chrome://global/skin/icons/error.svg chrome://global/skin/icons/experiments.svg chrome://global/skin/icons/folder.svg chrome://global/skin/icons/heart.svg chrome://global/skin/icons/help.svg chrome://global/skin/icons/highlights.svg chrome://global/skin/icons/indicator-private-browsing.svg chrome://global/skin/icons/info-filled.svg chrome://global/skin/icons/info.svg chrome://global/skin/icons/lightbulb.svg chrome://global/skin/icons/link.svg chrome://global/skin/icons/loading.png chrome://global/skin/icons/loading.svg chrome://global/skin/icons/loading@2x.png chrome://global/skin/icons/mdn.svg chrome://global/skin/icons/menu-check.svg chrome://global/skin/icons/minus.svg chrome://global/skin/icons/more.svg chrome://global/skin/icons/newsfeed.svg chrome://global/skin/icons/open-in-new.svg chrome://global/skin/icons/page-landscape.svg chrome://global/skin/icons/page-portrait.svg chrome://global/skin/icons/pendingpaint.png chrome://global/skin/icons/performance.svg chrome://global/skin/icons/plugin.svg chrome://global/skin/icons/plus-20.svg chrome://global/skin/icons/plus.svg chrome://global/skin/icons/pocket-favicon.ico chrome://global/skin/icons/pocket-outline.svg chrome://global/skin/icons/pocket.svg chrome://global/skin/icons/print.svg chrome://global/skin/icons/question-64.png chrome://global/skin/icons/rating-star.svg chrome://global/skin/icons/reload.svg chrome://global/skin/icons/resizer.svg chrome://global/skin/icons/search-glass.svg chrome://global/skin/icons/search-textbox.svg chrome://global/skin/icons/security-broken.svg chrome://global/skin/icons/security-warning.svg chrome://global/skin/icons/security.svg chrome://global/skin/icons/settings.svg chrome://global/skin/icons/sort-arrow.svg chrome://global/skin/icons/thumbs-down-20.svg chrome://global/skin/icons/thumbs-up-20.svg chrome://global/skin/icons/trending.svg chrome://global/skin/icons/undo.svg chrome://global/skin/icons/update-icon.svg chrome://global/skin/icons/warning-64.png chrome://global/skin/icons/warning-fill-12.svg chrome://global/skin/icons/warning-large.png chrome://global/skin/icons/warning.svg chrome://global/skin/illustrations/about-license.svg chrome://global/skin/illustrations/about-rights.svg chrome://global/skin/illustrations/error-malformed-url.svg chrome://global/skin/in-content/common-shared.css chrome://global/skin/in-content/common.css chrome://global/skin/in-content/info-pages.css chrome://global/skin/in-content/wifi.svg chrome://global/skin/media/audio-muted.svg chrome://global/skin/media/audio.svg chrome://global/skin/media/audioNoAudioButton.svg chrome://global/skin/media/castingButton-active.svg chrome://global/skin/media/castingButton-ready.svg chrome://global/skin/media/closed-caption-settings-button.svg chrome://global/skin/media/closedCaptionButton-cc-off.svg chrome://global/skin/media/closedCaptionButton-cc-on.svg chrome://global/skin/media/error.png chrome://global/skin/media/fullscreenEnterButton.svg chrome://global/skin/media/fullscreenExitButton.svg chrome://global/skin/media/imagedoc-darknoise.png chrome://global/skin/media/imagedoc-lightnoise.png chrome://global/skin/media/pause-fill.svg chrome://global/skin/media/picture-in-picture-closed.svg chrome://global/skin/media/picture-in-picture-enter-fullscreen-button.svg chrome://global/skin/media/picture-in-picture-exit-fullscreen-button.svg chrome://global/skin/media/picture-in-picture-open.svg chrome://global/skin/media/picture-in-picture-seekBackward-button.svg chrome://global/skin/media/picture-in-picture-seekForward-button.svg chrome://global/skin/media/pipToggle.css chrome://global/skin/media/play-fill.svg chrome://global/skin/media/stalled.png chrome://global/skin/media/textrecognition.css chrome://global/skin/media/throbber.png chrome://global/skin/media/videocontrols.css chrome://global/skin/menu-shared.css chrome://global/skin/menu.css chrome://global/skin/menulist-shared.css chrome://global/skin/menulist.css chrome://global/skin/narrate-improved.css chrome://global/skin/narrate.css chrome://global/skin/narrate/arrow.svg chrome://global/skin/narrate/back.svg chrome://global/skin/narrate/fast.svg chrome://global/skin/narrate/forward.svg chrome://global/skin/narrate/headphone-active.svg chrome://global/skin/narrate/headphone.svg chrome://global/skin/narrate/skip-backward-20.svg chrome://global/skin/narrate/skip-forward-20.svg chrome://global/skin/narrate/slow.svg chrome://global/skin/narrate/start.svg chrome://global/skin/narrate/stop.svg chrome://global/skin/notification.css chrome://global/skin/numberinput.css chrome://global/skin/offlineSupportPages.css chrome://global/skin/pictureinpicture/player.css chrome://global/skin/pictureinpicture/texttracks.css chrome://global/skin/popup.css chrome://global/skin/popupnotification.css chrome://global/skin/printPageSetup.css chrome://global/skin/radio.css chrome://global/skin/reader/RM-Content-Width-Minus-42x16.svg chrome://global/skin/reader/RM-Content-Width-Plus-44x16.svg chrome://global/skin/reader/RM-Line-Height-Minus-38x14.svg chrome://global/skin/reader/RM-Line-Height-Plus-38x24.svg chrome://global/skin/reader/RM-Minus-24x24.svg chrome://global/skin/reader/RM-Plus-24x24.svg chrome://global/skin/reader/RM-Sans-Serif.svg chrome://global/skin/reader/RM-Serif.svg chrome://global/skin/reader/RM-Type-Controls-24x24.svg chrome://global/skin/reader/align-center-20.svg chrome://global/skin/reader/align-left-20.svg chrome://global/skin/reader/align-right-20.svg chrome://global/skin/reader/character-spacing-20.svg chrome://global/skin/reader/content-width-20.svg chrome://global/skin/reader/line-spacing-20.svg chrome://global/skin/reader/word-spacing-20.svg chrome://global/skin/richlistbox.css chrome://global/skin/search-textbox.css chrome://global/skin/splitter.css chrome://global/skin/tabbox.css chrome://global/skin/toolbar.css chrome://global/skin/toolbarbutton.css chrome://global/skin/tree/sort-asc.svg chrome://global/skin/tree/sort-dsc.svg chrome://global/skin/tree/tree.css chrome://global/skin/wizard.css chrome://pocket/content/Pocket.sys.mjs chrome://pocket/content/SaveToPocket.sys.mjs chrome://pocket/content/panels/css/global.scss chrome://pocket/content/panels/css/home.scss chrome://pocket/content/panels/css/main.compiled.css chrome://pocket/content/panels/css/main.scss chrome://pocket/content/panels/css/normalize.scss chrome://pocket/content/panels/css/panel.scss chrome://pocket/content/panels/css/saved.scss chrome://pocket/content/panels/css/signup.scss chrome://pocket/content/panels/css/styleguide.scss chrome://pocket/content/panels/fonts/FiraSans-Regular.woff chrome://pocket/content/panels/home.html chrome://pocket/content/panels/img/chevron-right.svg chrome://pocket/content/panels/img/list-view.svg chrome://pocket/content/panels/img/open.svg chrome://pocket/content/panels/img/pocketerror@1x.png chrome://pocket/content/panels/img/pocketerror@2x.png chrome://pocket/content/panels/img/pocketlogo-dark.svg chrome://pocket/content/panels/img/pocketlogo.svg chrome://pocket/content/panels/img/pocketlogo@1x.png chrome://pocket/content/panels/img/pocketlogo@2x.png chrome://pocket/content/panels/img/pocketlogosolo@1x.png chrome://pocket/content/panels/img/pocketlogosolo@2x.png chrome://pocket/content/panels/img/pocketsignup_button@1x.png chrome://pocket/content/panels/img/pocketsignup_button@2x.png chrome://pocket/content/panels/img/pocketsignup_devices@1x.png chrome://pocket/content/panels/img/pocketsignup_devices@2x.png chrome://pocket/content/panels/img/pocketsignup_hero@1x.png chrome://pocket/content/panels/img/pocketsignup_hero@2x.png chrome://pocket/content/panels/img/rainbow-reader.svg chrome://pocket/content/panels/img/signup_firefoxlogo@1x.png chrome://pocket/content/panels/img/signup_firefoxlogo@2x.png chrome://pocket/content/panels/img/signup_help@1x.png chrome://pocket/content/panels/img/signup_help@2x.png chrome://pocket/content/panels/img/tag_close@1x.png chrome://pocket/content/panels/img/tag_close@2x.png chrome://pocket/content/panels/img/tag_closeactive@1x.png chrome://pocket/content/panels/img/tag_closeactive@2x.png chrome://pocket/content/panels/js/home/entry.js chrome://pocket/content/panels/js/main.bundle.js chrome://pocket/content/panels/js/saved/entry.js chrome://pocket/content/panels/js/signup/entry.js chrome://pocket/content/panels/js/style-guide/entry.js chrome://pocket/content/panels/js/vendor.bundle.js chrome://pocket/content/panels/saved.html chrome://pocket/content/panels/signup.html chrome://pocket/content/panels/style-guide.html chrome://pocket/content/pktApi.sys.mjs chrome://pocket/content/pktTelemetry.sys.mjs chrome://pocket/content/pktUI.js resource://activity-stream/common/Actions.mjs resource://activity-stream/common/Dedupe.sys.mjs resource://activity-stream/common/Reducers.sys.mjs resource://activity-stream/data/content/abouthomecache/page.html.template resource://activity-stream/data/content/abouthomecache/script.js.template resource://activity-stream/data/content/activity-stream.bundle.js resource://activity-stream/data/content/newtab-render.js resource://activity-stream/data/custom-elements/paragraph.js resource://activity-stream/lib/AboutPreferences.sys.mjs resource://activity-stream/lib/ActivityStream.sys.mjs resource://activity-stream/lib/ActivityStreamMessageChannel.sys.mjs resource://activity-stream/lib/ActivityStreamPrefs.sys.mjs resource://activity-stream/lib/ActivityStreamStorage.sys.mjs resource://activity-stream/lib/DefaultSites.sys.mjs resource://activity-stream/lib/DiscoveryStreamFeed.sys.mjs resource://activity-stream/lib/DownloadsManager.sys.mjs resource://activity-stream/lib/FaviconFeed.sys.mjs resource://activity-stream/lib/FilterAdult.sys.mjs resource://activity-stream/lib/HighlightsFeed.sys.mjs resource://activity-stream/lib/LinksCache.sys.mjs resource://activity-stream/lib/NewTabInit.sys.mjs resource://activity-stream/lib/PersistentCache.sys.mjs resource://activity-stream/lib/PersonalityProvider/NaiveBayesTextTagger.mjs resource://activity-stream/lib/PersonalityProvider/NmfTextTagger.mjs resource://activity-stream/lib/PersonalityProvider/PersonalityProvider.sys.mjs resource://activity-stream/lib/PersonalityProvider/PersonalityProvider.worker.mjs resource://activity-stream/lib/PersonalityProvider/PersonalityProviderWorkerClass.mjs resource://activity-stream/lib/PersonalityProvider/RecipeExecutor.mjs resource://activity-stream/lib/PersonalityProvider/Tokenize.mjs resource://activity-stream/lib/PlacesFeed.sys.mjs resource://activity-stream/lib/PrefsFeed.sys.mjs resource://activity-stream/lib/RecommendationProvider.sys.mjs resource://activity-stream/lib/Screenshots.sys.mjs resource://activity-stream/lib/SearchShortcuts.sys.mjs resource://activity-stream/lib/SectionsManager.sys.mjs resource://activity-stream/lib/ShortURL.sys.mjs resource://activity-stream/lib/SiteClassifier.sys.mjs resource://activity-stream/lib/Store.sys.mjs resource://activity-stream/lib/SystemTickFeed.sys.mjs resource://activity-stream/lib/TelemetryFeed.sys.mjs resource://activity-stream/lib/TippyTopProvider.sys.mjs resource://activity-stream/lib/TopSitesFeed.sys.mjs resource://activity-stream/lib/TopStoriesFeed.sys.mjs resource://activity-stream/lib/UTEventReporting.sys.mjs resource://activity-stream/lib/WallpaperFeed.sys.mjs resource://activity-stream/lib/WeatherFeed.sys.mjs resource://activity-stream/lib/cache.worker.js resource://activity-stream/prerendered/activity-stream-noscripts.html resource://activity-stream/prerendered/activity-stream.html resource://activity-stream/vendor/Redux.sys.mjs resource://activity-stream/vendor/prop-types.js resource://activity-stream/vendor/react-dom-server.js resource://activity-stream/vendor/react-dom.js resource://activity-stream/vendor/react-redux.js resource://activity-stream/vendor/react-transition-group.js resource://activity-stream/vendor/react.js resource://activity-stream/vendor/redux.js resource://builtin-addons/search-detection/api.js resource://builtin-addons/search-detection/background.js resource://builtin-addons/search-detection/manifest.json resource://builtin-addons/search-detection/schema.json resource://content-accessible/ImageDocument.css resource://content-accessible/TopLevelImageDocument.css resource://content-accessible/TopLevelVideoDocument.css resource://content-accessible/accessiblecaret.css resource://content-accessible/details.css resource://content-accessible/html/folder.png resource://content-accessible/plaintext.css resource://content-accessible/searchfield-cancel.svg resource://content-accessible/viewsource.css resource://devtools-highlighter-styles/highlighters.css resource://devtools-shared-images/alert-small.svg resource://devtools-shared-images/command-pick-remote-touch.svg resource://devtools-shared-images/command-pick.svg resource://devtools-shared-images/error-small.svg resource://devtools-shared-images/info-small.svg resource://devtools-shared-images/resume.svg resource://devtools-shared-images/stepOver.svg resource://normandy-content/AboutPages.sys.mjs resource://normandy-content/ShieldFrameChild.sys.mjs resource://normandy-content/ShieldFrameParent.sys.mjs resource://normandy-content/about-studies/about-studies.css resource://normandy-content/about-studies/about-studies.html resource://normandy-content/about-studies/about-studies.js resource://normandy-vendor/LICENSE_THIRDPARTY resource://normandy-vendor/PropTypes.js resource://normandy-vendor/React.js resource://normandy-vendor/ReactDOM.js resource://normandy-vendor/classnames.js resource://search-extensions/1und1/favicon.ico resource://search-extensions/1und1/manifest.json resource://search-extensions/allegro-pl/favicon.ico resource://search-extensions/allegro-pl/manifest.json resource://search-extensions/amazon/_locales/au/messages.json resource://search-extensions/amazon/_locales/ca/messages.json resource://search-extensions/amazon/_locales/de/messages.json resource://search-extensions/amazon/_locales/en-GB/messages.json resource://search-extensions/amazon/_locales/france/messages.json resource://search-extensions/amazon/_locales/in/messages.json resource://search-extensions/amazon/_locales/it/messages.json resource://search-extensions/amazon/_locales/jp/messages.json resource://search-extensions/amazon/_locales/nl/messages.json resource://search-extensions/amazon/_locales/spain/messages.json resource://search-extensions/amazon/_locales/sweden/messages.json resource://search-extensions/amazon/favicon.ico resource://search-extensions/amazon/manifest.json resource://search-extensions/amazondotcn/_locales/default/messages.json resource://search-extensions/amazondotcn/_locales/mozillaonline/messages.json resource://search-extensions/amazondotcn/favicon.ico resource://search-extensions/amazondotcn/manifest.json resource://search-extensions/amazondotcom/_locales/en/messages.json resource://search-extensions/amazondotcom/_locales/us/messages.json resource://search-extensions/amazondotcom/favicon.ico resource://search-extensions/amazondotcom/manifest.json resource://search-extensions/azerdict/favicon.ico resource://search-extensions/azerdict/manifest.json resource://search-extensions/baidu/favicon.ico resource://search-extensions/baidu/manifest.json resource://search-extensions/bing/favicon.ico resource://search-extensions/bing/manifest.json resource://search-extensions/bok-NO/favicon.png resource://search-extensions/bok-NO/manifest.json resource://search-extensions/ceneji/favicon.png resource://search-extensions/ceneji/manifest.json resource://search-extensions/coccoc/favicon.ico resource://search-extensions/coccoc/manifest.json resource://search-extensions/daum-kr/favicon.ico resource://search-extensions/daum-kr/manifest.json resource://search-extensions/ddg/favicon.ico resource://search-extensions/ddg/manifest.json resource://search-extensions/ebay/_locales/at/messages.json resource://search-extensions/ebay/_locales/au/messages.json resource://search-extensions/ebay/_locales/be/messages.json resource://search-extensions/ebay/_locales/ca/messages.json resource://search-extensions/ebay/_locales/ch/messages.json resource://search-extensions/ebay/_locales/de/messages.json resource://search-extensions/ebay/_locales/en/messages.json resource://search-extensions/ebay/_locales/es/messages.json resource://search-extensions/ebay/_locales/fr/messages.json resource://search-extensions/ebay/_locales/ie/messages.json resource://search-extensions/ebay/_locales/it/messages.json resource://search-extensions/ebay/_locales/nl/messages.json resource://search-extensions/ebay/_locales/uk/messages.json resource://search-extensions/ebay/favicon.ico resource://search-extensions/ebay/manifest.json resource://search-extensions/ecosia/favicon.ico resource://search-extensions/ecosia/manifest.json resource://search-extensions/eudict/favicon.ico resource://search-extensions/eudict/manifest.json resource://search-extensions/faclair-beag/favicon.ico resource://search-extensions/faclair-beag/manifest.json resource://search-extensions/gmx/_locales/de/messages.json resource://search-extensions/gmx/_locales/en-GB/messages.json resource://search-extensions/gmx/_locales/es/messages.json resource://search-extensions/gmx/_locales/fr/messages.json resource://search-extensions/gmx/_locales/shopping/messages.json resource://search-extensions/gmx/favicon.png resource://search-extensions/gmx/manifest.json resource://search-extensions/google/_locales/en/messages.json resource://search-extensions/google/_locales/region-by/messages.json resource://search-extensions/google/_locales/region-kz/messages.json resource://search-extensions/google/_locales/region-ru/messages.json resource://search-extensions/google/_locales/region-tr/messages.json resource://search-extensions/google/favicon.ico resource://search-extensions/google/manifest.json resource://search-extensions/gulesider-NO/favicon.ico resource://search-extensions/gulesider-NO/manifest.json resource://search-extensions/leo_ende_de/favicon.png resource://search-extensions/leo_ende_de/manifest.json resource://search-extensions/longdo/favicon.ico resource://search-extensions/longdo/manifest.json resource://search-extensions/mailcom/favicon.ico resource://search-extensions/mailcom/manifest.json resource://search-extensions/mailru/_locales/default/messages.json resource://search-extensions/mailru/_locales/mailru001/messages.json resource://search-extensions/mailru/_locales/okru-az/messages.json resource://search-extensions/mailru/_locales/okru-en-US/messages.json resource://search-extensions/mailru/_locales/okru-hy-AM/messages.json resource://search-extensions/mailru/_locales/okru-kk/messages.json resource://search-extensions/mailru/_locales/okru-ro/messages.json resource://search-extensions/mailru/_locales/okru-ru/messages.json resource://search-extensions/mailru/_locales/okru-tr/messages.json resource://search-extensions/mailru/_locales/okru-uk/messages.json resource://search-extensions/mailru/_locales/okru-uz/messages.json resource://search-extensions/mailru/favicon.ico resource://search-extensions/mailru/manifest.json resource://search-extensions/mapy-cz/favicon.ico resource://search-extensions/mapy-cz/manifest.json resource://search-extensions/mercadolibre/_locales/ar/messages.json resource://search-extensions/mercadolibre/_locales/cl/messages.json resource://search-extensions/mercadolibre/_locales/mx/messages.json resource://search-extensions/mercadolibre/favicon.ico resource://search-extensions/mercadolibre/manifest.json resource://search-extensions/mercadolivre/favicon.ico resource://search-extensions/mercadolivre/manifest.json resource://search-extensions/naver-kr/favicon.ico resource://search-extensions/naver-kr/manifest.json resource://search-extensions/odpiralni/favicon.png resource://search-extensions/odpiralni/manifest.json resource://search-extensions/pazaruvaj/favicon.ico resource://search-extensions/pazaruvaj/manifest.json resource://search-extensions/priberam/favicon.png resource://search-extensions/priberam/manifest.json resource://search-extensions/prisjakt-sv-SE/favicon.ico resource://search-extensions/prisjakt-sv-SE/manifest.json resource://search-extensions/qwant/favicon.ico resource://search-extensions/qwant/manifest.json resource://search-extensions/qwantjr/favicon.ico resource://search-extensions/qwantjr/manifest.json resource://search-extensions/rakuten/favicon.ico resource://search-extensions/rakuten/manifest.json resource://search-extensions/readmoo/favicon.ico resource://search-extensions/readmoo/manifest.json resource://search-extensions/salidzinilv/favicon.ico resource://search-extensions/salidzinilv/manifest.json resource://search-extensions/seznam-cz/favicon.ico resource://search-extensions/seznam-cz/manifest.json resource://search-extensions/tyda-sv-SE/favicon.ico resource://search-extensions/tyda-sv-SE/manifest.json resource://search-extensions/vatera/favicon.ico resource://search-extensions/vatera/manifest.json resource://search-extensions/webde/favicon.ico resource://search-extensions/webde/manifest.json resource://search-extensions/wikipedia/_locales/NN/messages.json resource://search-extensions/wikipedia/_locales/NO/messages.json resource://search-extensions/wikipedia/_locales/af/messages.json resource://search-extensions/wikipedia/_locales/an/messages.json resource://search-extensions/wikipedia/_locales/ar/messages.json resource://search-extensions/wikipedia/_locales/ast/messages.json resource://search-extensions/wikipedia/_locales/az/messages.json resource://search-extensions/wikipedia/_locales/be-tarask/messages.json resource://search-extensions/wikipedia/_locales/be/messages.json resource://search-extensions/wikipedia/_locales/bg/messages.json resource://search-extensions/wikipedia/_locales/bn/messages.json resource://search-extensions/wikipedia/_locales/br/messages.json resource://search-extensions/wikipedia/_locales/bs/messages.json resource://search-extensions/wikipedia/_locales/ca/messages.json resource://search-extensions/wikipedia/_locales/cy/messages.json resource://search-extensions/wikipedia/_locales/cz/messages.json resource://search-extensions/wikipedia/_locales/da/messages.json resource://search-extensions/wikipedia/_locales/de/messages.json resource://search-extensions/wikipedia/_locales/dsb/messages.json resource://search-extensions/wikipedia/_locales/el/messages.json resource://search-extensions/wikipedia/_locales/en/messages.json resource://search-extensions/wikipedia/_locales/eo/messages.json resource://search-extensions/wikipedia/_locales/es/messages.json resource://search-extensions/wikipedia/_locales/et/messages.json resource://search-extensions/wikipedia/_locales/eu/messages.json resource://search-extensions/wikipedia/_locales/fa/messages.json resource://search-extensions/wikipedia/_locales/fi/messages.json resource://search-extensions/wikipedia/_locales/fr/messages.json resource://search-extensions/wikipedia/_locales/fy-NL/messages.json resource://search-extensions/wikipedia/_locales/ga-IE/messages.json resource://search-extensions/wikipedia/_locales/gd/messages.json resource://search-extensions/wikipedia/_locales/gl/messages.json resource://search-extensions/wikipedia/_locales/gn/messages.json resource://search-extensions/wikipedia/_locales/gu/messages.json resource://search-extensions/wikipedia/_locales/he/messages.json resource://search-extensions/wikipedia/_locales/hi/messages.json resource://search-extensions/wikipedia/_locales/hr/messages.json resource://search-extensions/wikipedia/_locales/hsb/messages.json resource://search-extensions/wikipedia/_locales/hu/messages.json resource://search-extensions/wikipedia/_locales/hy/messages.json resource://search-extensions/wikipedia/_locales/ia/messages.json resource://search-extensions/wikipedia/_locales/id/messages.json resource://search-extensions/wikipedia/_locales/is/messages.json resource://search-extensions/wikipedia/_locales/it/messages.json resource://search-extensions/wikipedia/_locales/ja/messages.json resource://search-extensions/wikipedia/_locales/ka/messages.json resource://search-extensions/wikipedia/_locales/kab/messages.json resource://search-extensions/wikipedia/_locales/kk/messages.json resource://search-extensions/wikipedia/_locales/km/messages.json resource://search-extensions/wikipedia/_locales/kn/messages.json resource://search-extensions/wikipedia/_locales/kr/messages.json resource://search-extensions/wikipedia/_locales/lij/messages.json resource://search-extensions/wikipedia/_locales/lo/messages.json resource://search-extensions/wikipedia/_locales/lt/messages.json resource://search-extensions/wikipedia/_locales/ltg/messages.json resource://search-extensions/wikipedia/_locales/lv/messages.json resource://search-extensions/wikipedia/_locales/mk/messages.json resource://search-extensions/wikipedia/_locales/mr/messages.json resource://search-extensions/wikipedia/_locales/ms/messages.json resource://search-extensions/wikipedia/_locales/my/messages.json resource://search-extensions/wikipedia/_locales/ne/messages.json resource://search-extensions/wikipedia/_locales/nl/messages.json resource://search-extensions/wikipedia/_locales/oc/messages.json resource://search-extensions/wikipedia/_locales/pa/messages.json resource://search-extensions/wikipedia/_locales/pl/messages.json resource://search-extensions/wikipedia/_locales/pt/messages.json resource://search-extensions/wikipedia/_locales/rm/messages.json resource://search-extensions/wikipedia/_locales/ro/messages.json resource://search-extensions/wikipedia/_locales/ru/messages.json resource://search-extensions/wikipedia/_locales/si/messages.json resource://search-extensions/wikipedia/_locales/sk/messages.json resource://search-extensions/wikipedia/_locales/sl/messages.json resource://search-extensions/wikipedia/_locales/sq/messages.json resource://search-extensions/wikipedia/_locales/sr/messages.json resource://search-extensions/wikipedia/_locales/sv-SE/messages.json resource://search-extensions/wikipedia/_locales/ta/messages.json resource://search-extensions/wikipedia/_locales/te/messages.json resource://search-extensions/wikipedia/_locales/th/messages.json resource://search-extensions/wikipedia/_locales/tl/messages.json resource://search-extensions/wikipedia/_locales/tr/messages.json resource://search-extensions/wikipedia/_locales/uk/messages.json resource://search-extensions/wikipedia/_locales/ur/messages.json resource://search-extensions/wikipedia/_locales/uz/messages.json resource://search-extensions/wikipedia/_locales/vi/messages.json resource://search-extensions/wikipedia/_locales/wo/messages.json resource://search-extensions/wikipedia/_locales/zh-CN/messages.json resource://search-extensions/wikipedia/_locales/zh-TW/messages.json resource://search-extensions/wikipedia/favicon.ico resource://search-extensions/wikipedia/manifest.json resource://search-extensions/wiktionary/_locales/oc/messages.json resource://search-extensions/wiktionary/_locales/te/messages.json resource://search-extensions/wiktionary/favicon.ico resource://search-extensions/wiktionary/manifest.json resource://search-extensions/wolnelektury-pl/favicon.png resource://search-extensions/wolnelektury-pl/manifest.json resource://search-extensions/yahoo-jp-auctions/favicon.ico resource://search-extensions/yahoo-jp-auctions/manifest.json resource://search-extensions/yahoo-jp/favicon.ico resource://search-extensions/yahoo-jp/manifest.json resource://search-extensions/yandex/_locales/az/messages.json resource://search-extensions/yandex/_locales/by/messages.json resource://search-extensions/yandex/_locales/en/messages.json resource://search-extensions/yandex/_locales/kk/messages.json resource://search-extensions/yandex/_locales/ru/messages.json resource://search-extensions/yandex/_locales/tr/messages.json resource://search-extensions/yandex/_locales/ua/messages.json resource://search-extensions/yandex/manifest.json resource://search-extensions/yandex/yandex-en.ico resource://search-extensions/yandex/yandex-ru.ico resource://usercontext-content/briefcase.svg resource://usercontext-content/builtin-themes/alpenglow/background-gradient-dark.svg resource://usercontext-content/builtin-themes/alpenglow/background-gradient.svg resource://usercontext-content/builtin-themes/alpenglow/background-noodles-left-dark.svg resource://usercontext-content/builtin-themes/alpenglow/background-noodles-left.svg resource://usercontext-content/builtin-themes/alpenglow/background-noodles-right-dark.svg resource://usercontext-content/builtin-themes/alpenglow/background-noodles-right.svg resource://usercontext-content/builtin-themes/alpenglow/icon.svg resource://usercontext-content/builtin-themes/alpenglow/manifest.json resource://usercontext-content/builtin-themes/alpenglow/preview.svg resource://usercontext-content/builtin-themes/colorways/2021abstract/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2021abstract/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2021abstract/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2021abstract/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2021abstract/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2021abstract/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2021abstract/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2021abstract/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2021abstract/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2021cheers/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2021cheers/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2021cheers/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2021cheers/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2021cheers/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2021cheers/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2021cheers/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2021cheers/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2021cheers/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2021elemental/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2021elemental/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2021elemental/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2021elemental/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2021elemental/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2021elemental/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2021elemental/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2021elemental/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2021elemental/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2021foto/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2021foto/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2021foto/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2021foto/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2021foto/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2021foto/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2021foto/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2021foto/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2021foto/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2021graffiti/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2021graffiti/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2021graffiti/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2021graffiti/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2021graffiti/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2021graffiti/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2021graffiti/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2021graffiti/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2021graffiti/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2021lush/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2021lush/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2021lush/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2021lush/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2021lush/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2021lush/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2021lush/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2021lush/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2021lush/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2022activist/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2022activist/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2022activist/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2022activist/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2022activist/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2022activist/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2022activist/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2022activist/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2022activist/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2022blue/icon.svg resource://usercontext-content/builtin-themes/colorways/2022blue/manifest.json resource://usercontext-content/builtin-themes/colorways/2022blue/preview.svg resource://usercontext-content/builtin-themes/colorways/2022dreamer/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2022dreamer/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2022dreamer/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2022dreamer/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2022dreamer/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2022dreamer/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2022dreamer/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2022dreamer/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2022dreamer/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2022expressionist/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2022expressionist/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2022expressionist/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2022expressionist/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2022expressionist/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2022expressionist/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2022expressionist/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2022expressionist/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2022expressionist/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2022green/icon.svg resource://usercontext-content/builtin-themes/colorways/2022green/manifest.json resource://usercontext-content/builtin-themes/colorways/2022green/preview.svg resource://usercontext-content/builtin-themes/colorways/2022innovator/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2022innovator/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2022innovator/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2022innovator/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2022innovator/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2022innovator/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2022innovator/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2022innovator/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2022innovator/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2022orange/icon.svg resource://usercontext-content/builtin-themes/colorways/2022orange/manifest.json resource://usercontext-content/builtin-themes/colorways/2022orange/preview.svg resource://usercontext-content/builtin-themes/colorways/2022playmaker/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2022playmaker/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2022playmaker/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2022playmaker/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2022playmaker/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2022playmaker/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2022playmaker/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2022playmaker/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2022playmaker/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2022purple/icon.svg resource://usercontext-content/builtin-themes/colorways/2022purple/manifest.json resource://usercontext-content/builtin-themes/colorways/2022purple/preview.svg resource://usercontext-content/builtin-themes/colorways/2022red/icon.svg resource://usercontext-content/builtin-themes/colorways/2022red/manifest.json resource://usercontext-content/builtin-themes/colorways/2022red/preview.svg resource://usercontext-content/builtin-themes/colorways/2022visionary/balanced/icon.svg resource://usercontext-content/builtin-themes/colorways/2022visionary/balanced/manifest.json resource://usercontext-content/builtin-themes/colorways/2022visionary/balanced/preview.svg resource://usercontext-content/builtin-themes/colorways/2022visionary/bold/icon.svg resource://usercontext-content/builtin-themes/colorways/2022visionary/bold/manifest.json resource://usercontext-content/builtin-themes/colorways/2022visionary/bold/preview.svg resource://usercontext-content/builtin-themes/colorways/2022visionary/soft/icon.svg resource://usercontext-content/builtin-themes/colorways/2022visionary/soft/manifest.json resource://usercontext-content/builtin-themes/colorways/2022visionary/soft/preview.svg resource://usercontext-content/builtin-themes/colorways/2022yellow/icon.svg resource://usercontext-content/builtin-themes/colorways/2022yellow/manifest.json resource://usercontext-content/builtin-themes/colorways/2022yellow/preview.svg resource://usercontext-content/builtin-themes/dark/experiment.css resource://usercontext-content/builtin-themes/dark/icon.svg resource://usercontext-content/builtin-themes/dark/manifest.json resource://usercontext-content/builtin-themes/dark/preview.svg resource://usercontext-content/builtin-themes/light/experiment.css resource://usercontext-content/builtin-themes/light/icon.svg resource://usercontext-content/builtin-themes/light/manifest.json resource://usercontext-content/builtin-themes/light/preview.svg resource://usercontext-content/cart.svg resource://usercontext-content/chill.svg resource://usercontext-content/circle.svg resource://usercontext-content/dollar.svg resource://usercontext-content/fence.svg resource://usercontext-content/fingerprint.svg resource://usercontext-content/food.svg resource://usercontext-content/fruit.svg resource://usercontext-content/gift.svg resource://usercontext-content/pet.svg resource://usercontext-content/tree.svg resource://usercontext-content/vacation.svg ` let listTor = ` chrome://branding/content/icon256.png chrome://browser/content/aboutDialogTor.css chrome://browser/content/abouttbupdate/aboutTBUpdate.css chrome://browser/content/abouttbupdate/aboutTBUpdate.js chrome://browser/content/abouttbupdate/aboutTBUpdate.xhtml chrome://browser/content/abouttor/1f4e3-megaphone.svg chrome://browser/content/abouttor/26a1-high-voltage.svg chrome://browser/content/abouttor/2728-sparkles.svg chrome://browser/content/abouttor/2764-red-heart.svg chrome://browser/content/abouttor/aboutTor.css chrome://browser/content/abouttor/aboutTor.html chrome://browser/content/abouttor/aboutTor.js chrome://browser/content/abouttor/dax-logo.svg chrome://browser/content/abouttor/yec-2024-browse.svg chrome://browser/content/abouttor/yec-2024-fonts.css chrome://browser/content/abouttor/yec-2024-heart.svg chrome://browser/content/abouttor/yec-2024-search.svg chrome://browser/content/abouttor/yec-2024-speak.svg chrome://browser/content/languageNotification.js chrome://browser/content/manual/ar.html chrome://browser/content/manual/be.html chrome://browser/content/manual/bg.html chrome://browser/content/manual/bn.html chrome://browser/content/manual/ca.html chrome://browser/content/manual/de.html chrome://browser/content/manual/el.html chrome://browser/content/manual/en.html chrome://browser/content/manual/es.html chrome://browser/content/manual/fa.html chrome://browser/content/manual/fi.html chrome://browser/content/manual/fr.html chrome://browser/content/manual/ga.html chrome://browser/content/manual/he.html chrome://browser/content/manual/hu.html chrome://browser/content/manual/id.html chrome://browser/content/manual/is.html chrome://browser/content/manual/it.html chrome://browser/content/manual/ja.html chrome://browser/content/manual/ka.html chrome://browser/content/manual/km.html chrome://browser/content/manual/ko.html chrome://browser/content/manual/lt.html chrome://browser/content/manual/mk.html chrome://browser/content/manual/my.html chrome://browser/content/manual/pl.html chrome://browser/content/manual/ps.html chrome://browser/content/manual/pt-BR.html chrome://browser/content/manual/pt-PT.html chrome://browser/content/manual/ro.html chrome://browser/content/manual/ru.html chrome://browser/content/manual/sq.html chrome://browser/content/manual/static/collapse.min.js chrome://browser/content/manual/static/css/bootstrap-grid.css chrome://browser/content/manual/static/css/bootstrap-reboot.css chrome://browser/content/manual/static/css/bootstrap.css chrome://browser/content/manual/static/images/android-configure.png chrome://browser/content/manual/static/images/android-connect.png chrome://browser/content/manual/static/images/android-new-circuit.png chrome://browser/content/manual/static/images/android-provide-a-bridge.png chrome://browser/content/manual/static/images/android-provided-a-bridge.png chrome://browser/content/manual/static/images/android-security-settings.gif chrome://browser/content/manual/static/images/android-select-a-bridge.png chrome://browser/content/manual/static/images/android-selected-a-bridge.png chrome://browser/content/manual/static/images/android-uninstall-device-settings.png chrome://browser/content/manual/static/images/android-uninstall-f-droid.png chrome://browser/content/manual/static/images/android-uninstall-google-play.png chrome://browser/content/manual/static/images/android-update-f-droid.png chrome://browser/content/manual/static/images/android-update-google-play.png chrome://browser/content/manual/static/images/android-view-logs.png chrome://browser/content/manual/static/images/bridge-qr.png chrome://browser/content/manual/static/images/bridgemoji.png chrome://browser/content/manual/static/images/built-in-bridge.png chrome://browser/content/manual/static/images/circuit_full.png chrome://browser/content/manual/static/images/client-auth.png chrome://browser/content/manual/static/images/configure.png chrome://browser/content/manual/static/images/connect.png chrome://browser/content/manual/static/images/connection-assist-auto.png chrome://browser/content/manual/static/images/connection-assist-offline.png chrome://browser/content/manual/static/images/connection-assist-select.png chrome://browser/content/manual/static/images/connection-assist-test.png chrome://browser/content/manual/static/images/connection-test-failure.png chrome://browser/content/manual/static/images/connection-test-success.png chrome://browser/content/manual/static/images/cryptocurrency-safety.png chrome://browser/content/manual/static/images/gettor-bot-telegram.png chrome://browser/content/manual/static/images/how-tor-works.png chrome://browser/content/manual/static/images/http-website-error.png chrome://browser/content/manual/static/images/https-only-mode.png chrome://browser/content/manual/static/images/letterboxing.png chrome://browser/content/manual/static/images/macos-go-to-folder-menu.png chrome://browser/content/manual/static/images/macos-go-to-folder-window.png chrome://browser/content/manual/static/images/new_identity.png chrome://browser/content/manual/static/images/onion-location.png chrome://browser/content/manual/static/images/pluggable-transport.png chrome://browser/content/manual/static/images/provide-bridge.png chrome://browser/content/manual/static/images/proxy.png chrome://browser/content/manual/static/images/quickstart.png chrome://browser/content/manual/static/images/request-a-bridge.png chrome://browser/content/manual/static/images/security-settings-anim.gif chrome://browser/content/manual/static/images/security-settings-safest.png chrome://browser/content/manual/static/images/tor-https-0.png chrome://browser/content/manual/static/images/tor-https-1.png chrome://browser/content/manual/static/images/tor-https-2.png chrome://browser/content/manual/static/images/tor-https-3.png chrome://browser/content/manual/static/images/update1.png chrome://browser/content/manual/static/images/update4.png chrome://browser/content/manual/static/js/anchor.min.js chrome://browser/content/manual/static/js/bootstrap.bundle.js chrome://browser/content/manual/static/js/bootstrap.bundle.min.js chrome://browser/content/manual/static/js/bootstrap.js chrome://browser/content/manual/static/js/bootstrap.min.js chrome://browser/content/manual/static/js/clipboard.min.js chrome://browser/content/manual/static/js/collapse.min.js chrome://browser/content/manual/static/js/download.js chrome://browser/content/manual/static/js/errors.js chrome://browser/content/manual/static/js/fallback.js chrome://browser/content/manual/static/js/holder.min.js chrome://browser/content/manual/static/js/jquery-3.2.1.min.js chrome://browser/content/manual/static/js/jquery-slim.min.js chrome://browser/content/manual/static/js/modernizr.js chrome://browser/content/manual/static/js/popper.min.js chrome://browser/content/manual/static/js/scrollspy.min.js chrome://browser/content/manual/static/js/util.min.js chrome://browser/content/manual/sw.html chrome://browser/content/manual/th.html chrome://browser/content/manual/tk.html chrome://browser/content/manual/tr.html chrome://browser/content/manual/uk.html chrome://browser/content/manual/vi.html chrome://browser/content/manual/wo.html chrome://browser/content/manual/zh-CN.html chrome://browser/content/manual/zh-TW.html chrome://browser/content/newIdentityDialog.css chrome://browser/content/newIdentityDialog.js chrome://browser/content/newIdentityDialog.xhtml chrome://browser/content/newidentity.js chrome://browser/content/onionservices/authPreferences.css chrome://browser/content/onionservices/authPreferences.js chrome://browser/content/onionservices/authPrompt.js chrome://browser/content/onionservices/onionservices.css chrome://browser/content/onionservices/savedKeysDialog.js chrome://browser/content/onionservices/savedKeysDialog.xhtml chrome://browser/content/preferences/letterboxing-middle-dark.svg chrome://browser/content/preferences/letterboxing-middle-light.svg chrome://browser/content/preferences/letterboxing-top-dark.svg chrome://browser/content/preferences/letterboxing-top-light.svg chrome://browser/content/preferences/letterboxing.css chrome://browser/content/preferences/letterboxing.js chrome://browser/content/rulesets/aboutRulesets.css chrome://browser/content/rulesets/aboutRulesets.html chrome://browser/content/rulesets/aboutRulesets.js chrome://browser/content/rulesets/securedrop.svg chrome://browser/content/securitylevel/securityLevel.js chrome://browser/content/securitylevel/securityLevelButton.css chrome://browser/content/securitylevel/securityLevelIcon.svg chrome://browser/content/securitylevel/securityLevelPanel.css chrome://browser/content/securitylevel/securityLevelPreferences.css chrome://browser/content/tor-circuit-flags/1f1e6-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1f6.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1fd.svg chrome://browser/content/tor-circuit-flags/1f1e6-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1e7.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1ef.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f6.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1fb.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1e7-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1f5.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1fb.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1fd.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1e8-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1e9-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1e9-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1e9-1f1ef.svg chrome://browser/content/tor-circuit-flags/1f1e9-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1e9-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1e9-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1e9-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1ea-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1eb-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1eb-1f1ef.svg chrome://browser/content/tor-circuit-flags/1f1eb-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1eb-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1eb-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1eb-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1e7.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f5.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f6.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1ec-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1ed-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1ed-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1ed-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1ed-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1ed-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1ed-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f6.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1ee-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1ef-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1ef-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1ef-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1ef-1f1f5.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1f5.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1f0-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1e7.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1fb.svg chrome://browser/content/tor-circuit-flags/1f1f1-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f5.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f6.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1fb.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1fd.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1f2-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1f5.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1f3-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1f4-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1f5-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1f6-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1f7-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1f7-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1f7-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1f7-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1f7-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1e7.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1ef.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1fb.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1fd.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1f8-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1e9.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1ed.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1ef.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1f1.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1f4.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1f7.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1fb.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1fc.svg chrome://browser/content/tor-circuit-flags/1f1f9-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1fa-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1fa-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1fa-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1fa-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1fa-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1fa-1f1fe.svg chrome://browser/content/tor-circuit-flags/1f1fa-1f1ff.svg chrome://browser/content/tor-circuit-flags/1f1fb-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1fb-1f1e8.svg chrome://browser/content/tor-circuit-flags/1f1fb-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1fb-1f1ec.svg chrome://browser/content/tor-circuit-flags/1f1fb-1f1ee.svg chrome://browser/content/tor-circuit-flags/1f1fb-1f1f3.svg chrome://browser/content/tor-circuit-flags/1f1fb-1f1fa.svg chrome://browser/content/tor-circuit-flags/1f1fc-1f1eb.svg chrome://browser/content/tor-circuit-flags/1f1fc-1f1f8.svg chrome://browser/content/tor-circuit-flags/1f1fd-1f1f0.svg chrome://browser/content/tor-circuit-flags/1f1fe-1f1ea.svg chrome://browser/content/tor-circuit-flags/1f1fe-1f1f9.svg chrome://browser/content/tor-circuit-flags/1f1ff-1f1e6.svg chrome://browser/content/tor-circuit-flags/1f1ff-1f1f2.svg chrome://browser/content/tor-circuit-flags/1f1ff-1f1fc.svg chrome://browser/content/tor-circuit-flags/README.txt chrome://browser/content/tor-circuit-icon-mask.svg chrome://browser/content/tor-circuit-node-end.svg chrome://browser/content/tor-circuit-node-middle.svg chrome://browser/content/tor-circuit-node-relays.svg chrome://browser/content/tor-circuit-node-start.svg chrome://browser/content/tor-circuit-redirect.svg chrome://browser/content/torCircuitPanel.css chrome://browser/content/torCircuitPanel.js chrome://browser/content/torpreferences/bridge-bot.svg chrome://browser/content/torpreferences/bridge-qr.svg chrome://browser/content/torpreferences/bridge.svg chrome://browser/content/torpreferences/bridgeQrDialog.js chrome://browser/content/torpreferences/bridgeQrDialog.xhtml chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js chrome://browser/content/torpreferences/bridgemoji/annotations.json chrome://browser/content/torpreferences/bridgemoji/bridge-emojis.json chrome://browser/content/torpreferences/bridgemoji/svgs/1f300.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f308.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f30a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f30b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f319.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f31f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f321.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f32d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f32e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f332.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f333.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f334.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f335.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f336.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f337.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f339.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f33a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f33b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f33d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f33f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f341.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f344.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f345.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f346.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f347.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f348.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f349.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f34a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f34b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f34c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f34d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f34f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f350.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f351.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f352.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f353.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f354.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f355.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f368.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f369.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f36a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f36b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f36c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f36d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f37f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f380.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f381.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f382.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f383.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f388.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f389.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f38f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f392.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f399.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f39f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3a0.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3a1.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3a2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3a8.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3ac.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3af.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3b2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3b6.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3b7.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3b8.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3ba.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3bb.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3be.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3c0.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3c6.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3c8.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3d3.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3d4.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3d5.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3dd.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3e1.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3ee.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3f7.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3f8.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f3f9.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f40a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f40c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f40d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f417.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f418.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f419.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f41a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f41b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f41d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f41e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f41f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f420.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f422.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f425.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f426.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f428.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f42a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f42c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f42d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f42e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f42f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f430.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f431.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f432.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f433.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f434.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f435.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f436.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f437.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f43a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f43b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f43f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f441.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f451.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f455.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f457.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f45f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f47d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f484.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f488.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f48d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f48e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f490.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4a1.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4a7.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4b3.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4bf.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4cc.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4ce.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4d5.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4e1.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4e2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f4fb.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f50b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f511.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f525.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f526.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f52c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f52d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f52e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f54a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f58c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f58d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f5ff.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f680.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f681.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f686.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f68b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f68d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f695.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f697.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f69a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f69c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6a0.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6a2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6a4.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6f0.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6f4.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6f5.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6f6.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6f8.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6f9.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6fa.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f6fc.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f916.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f93f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f941.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f94c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f94f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f950.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f951.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f955.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f956.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f95c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f95d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f95e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f965.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f966.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f968.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f96c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f96d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f96f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f980.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f981.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f984.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f986.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f987.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f988.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f989.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f98a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f98b.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f98c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f98e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f98f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f992.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f993.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f994.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f995.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f998.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f999.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f99a.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f99c.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f99d.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f99e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9a3.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9a4.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9a5.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9a6.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9a7.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9a9.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9ad.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9c1.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9c3.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9c5.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9c7.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9c9.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9d9.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9da.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9dc.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9e0.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9e2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9e6.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9e9.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9ea.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9ec.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9ed.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9ee.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9f2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9f5.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1f9f9.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa73.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa80.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa81.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa83.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa90.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa91.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa95.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fa97.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fab6.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fad0.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fad2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/1fad6.svg chrome://browser/content/torpreferences/bridgemoji/svgs/23f0.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2600.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2602.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2604.svg chrome://browser/content/torpreferences/bridgemoji/svgs/260e.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2693.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2696.svg chrome://browser/content/torpreferences/bridgemoji/svgs/26bd.svg chrome://browser/content/torpreferences/bridgemoji/svgs/26f2.svg chrome://browser/content/torpreferences/bridgemoji/svgs/26f5.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2708.svg chrome://browser/content/torpreferences/bridgemoji/svgs/270f.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2728.svg chrome://browser/content/torpreferences/bridgemoji/svgs/2744.svg chrome://browser/content/torpreferences/builtinBridgeDialog.js chrome://browser/content/torpreferences/builtinBridgeDialog.xhtml chrome://browser/content/torpreferences/connectionCategory.inc.xhtml chrome://browser/content/torpreferences/connectionPane.js chrome://browser/content/torpreferences/connectionPane.xhtml chrome://browser/content/torpreferences/connectionSettingsDialog.js chrome://browser/content/torpreferences/connectionSettingsDialog.xhtml chrome://browser/content/torpreferences/lox-bridge-icon.svg chrome://browser/content/torpreferences/lox-bridge-pass.svg chrome://browser/content/torpreferences/lox-complete-ring.svg chrome://browser/content/torpreferences/lox-invite-icon.svg chrome://browser/content/torpreferences/lox-progress-ring.svg chrome://browser/content/torpreferences/lox-success.svg chrome://browser/content/torpreferences/loxInviteDialog.js chrome://browser/content/torpreferences/loxInviteDialog.xhtml chrome://browser/content/torpreferences/mail.svg chrome://browser/content/torpreferences/network-broken.svg chrome://browser/content/torpreferences/network.svg chrome://browser/content/torpreferences/provideBridgeDialog.js chrome://browser/content/torpreferences/provideBridgeDialog.xhtml chrome://browser/content/torpreferences/requestBridgeDialog.js chrome://browser/content/torpreferences/requestBridgeDialog.xhtml chrome://browser/content/torpreferences/telegram-logo.svg chrome://browser/content/torpreferences/torLogDialog.js chrome://browser/content/torpreferences/torLogDialog.xhtml chrome://browser/content/torpreferences/torPreferences.css chrome://browser/locale/aboutTBUpdate.dtd chrome://browser/skin/new_circuit.svg chrome://browser/skin/new_identity.svg chrome://browser/skin/onionlocation.css chrome://browser/skin/tor-branding.css chrome://browser/skin/tor-urlbar-button.css chrome://global/content/lox/lox_wasm_bg.wasm chrome://global/content/pt_config.json chrome://global/content/search/duckduckgo.ico chrome://global/content/search/startpage.png chrome://global/content/search/torBrowserSearchEngineIcons.json chrome://global/content/search/torBrowserSearchEngines.json chrome://global/content/search/wikipedia.ico chrome://global/content/torconnect/aboutTorConnect.css chrome://global/content/torconnect/aboutTorConnect.html chrome://global/content/torconnect/aboutTorConnect.js chrome://global/content/torconnect/arrow-right.svg chrome://global/content/torconnect/bridge.svg chrome://global/content/torconnect/connection-failure.svg chrome://global/content/torconnect/connection-location.svg chrome://global/content/torconnect/tor-connect-broken.svg chrome://global/content/torconnect/tor-connect.svg chrome://global/content/torconnect/tor-not-connected-to-connected-animated.svg chrome://global/content/torconnect/torConnectTitlebarStatus.css chrome://global/content/torconnect/torConnectTitlebarStatus.js chrome://global/content/torconnect/torConnectUrlbarButton.js chrome://global/skin/icons/onion-site.svg chrome://global/skin/icons/onion-slash.svg chrome://global/skin/icons/onion-warning.svg chrome://global/skin/icons/tor-dark-loading.png chrome://global/skin/icons/tor-dark-loading@2x.png chrome://global/skin/icons/tor-light-loading.png chrome://global/skin/icons/tor-light-loading@2x.png chrome://global/skin/onion-pattern.css chrome://global/skin/onion-pattern.svg chrome://global/skin/tor-colors.css resource://search-extensions/ddg-onion/favicon.ico resource://search-extensions/ddg-onion/manifest.json resource://search-extensions/startpage-onion/favicon.png resource://search-extensions/startpage-onion/manifest.json resource://search-extensions/startpage/favicon.png resource://search-extensions/startpage/manifest.json // Mullvad Browser chrome://browser/content/mullvad-browser/2728-sparkles.svg chrome://browser/content/mullvad-browser/aboutMullvadBrowser.css chrome://browser/content/mullvad-browser/aboutMullvadBrowser.js chrome://browser/content/mullvad-browser/aboutMullvadBrowser.xhtml chrome://browser/content/mullvad-browser/mullvadBrowserFont.css chrome://global/content/search/brave.svg chrome://global/content/search/metager.ico chrome://global/content/search/mojeek.ico chrome://global/content/search/mullvad-leta.svg chrome://global/content/search/mullvadBrowserSearchEngineIcons.json chrome://global/content/search/mullvadBrowserSearchEngines.json resource://search-extensions/brave/favicon.svg resource://search-extensions/brave/manifest.json resource://search-extensions/ddg-html/favicon.ico resource://search-extensions/ddg-html/manifest.json resource://search-extensions/metager/favicon.ico resource://search-extensions/metager/manifest.json resource://search-extensions/mojeek/favicon.ico resource://search-extensions/mojeek/manifest.json resource://search-extensions/mullvad-leta/favicon.svg resource://search-extensions/mullvad-leta/manifest.json ` </script> </body> </html> ================================================ FILE: tests/codecs_can_is.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>codecs: canPlay & isType</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#codecs">return to TZP index</a></td></tr> </table> <table id="tb13"> <col width="1%"><col width="24%"><col width="75%"> <thead><tr><th colspan="3"> <div class="nav-title">codecs: canPlay & isType <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="3" class="intro"> <span class="no_color">Testing <code>canPlayType</code> + <code>isTypeSupported</code> with large <span class='btn13 btnc mono' onClick='display(`audiolist`)'>audio</span> + <span class='btn13 btnc mono' onClick='display(`videolist`)'>video</span> lists </span> </td></tr> <tr><td colspan="3"><hr><br></td></tr> <tr><td colspan="2" class="padr">canPlayType</td><td class="mono"> <span class="c" id="audiocan"></span> | <span class="c" id="videocan"></span></td></tr> <tr><td colspan="2" class="padr">isTypeSupported</td><td class="mono"> <span class="c" id="audiotype"></span> | <span class="c" id="videotype"></span></td></tr> <tr><td colspan="3"><br><hr><br></td></tr> <tr><td></td> <td colspan="2" class="mono spaces" id="details"></td></tr> </table> <br> <script> 'use strict'; let v = "video/", a = "audio/" let audiolist = [ 'application/ogg', a+'3gpp', a+'3gpp2', a+'aac', a+'flac', a+'matroska', a+'mp3', a+'mp4', a+'mp4; codecs=', a+'mp4; codecs=""', a+'mp4; codecs="flac"', a+'mp4; codecs="mp3"', a+'mp4; codecs="opus"', a+'mp4; codecs=\'\'', a+'mpeg', a+'mpeg; codecs="mp3"', a+'ogg; codecs="flac"', a+'ogg; codecs="opus"', a+'ogg; codecs="vorbis"', a+'wav', a+'wav; codecs="1"', a+'wave', a+'wave; codecs="1"', a+'webm', a+'webm; codecs="opus"', a+'webm; codecs="vorbis"', a+'x-aac', a+'x-flac', a+'x-m4a', a+'x-matroska', a+'x-pn-wav', a+'x-pn-wav; codecs="1"', a+'x-wav', a+'x-wav; codecs="1"', a+'mp4; codecs="mp4a.40"', a+'mp4; codecs="mp4a.40.1"', a+'mp4; codecs="mp4a.40.2"', a+'mp4; codecs="mp4a.40.3"', a+'mp4; codecs="mp4a.40.4"', a+'mp4; codecs="mp4a.40.5"', a+'mp4; codecs="mp4a.40.6"', a+'mp4; codecs="mp4a.40.7"', a+'mp4; codecs="mp4a.40.8"', a+'mp4; codecs="mp4a.40.9"', a+'mp4; codecs="mp4a.40.12"', a+'mp4; codecs="mp4a.40.13"', a+'mp4; codecs="mp4a.40.14"', a+'mp4; codecs="mp4a.40.15"', a+'mp4; codecs="mp4a.40.16"', a+'mp4; codecs="mp4a.40.17"', a+'mp4; codecs="mp4a.40.19"', a+'mp4; codecs="mp4a.40.20"', a+'mp4; codecs="mp4a.40.21"', a+'mp4; codecs="mp4a.40.22"', a+'mp4; codecs="mp4a.40.23"', a+'mp4; codecs="mp4a.40.24"', a+'mp4; codecs="mp4a.40.25"', a+'mp4; codecs="mp4a.40.26"', a+'mp4; codecs="mp4a.40.27"', a+'mp4; codecs="mp4a.40.28"', a+'mp4; codecs="mp4a.40.29"', a+'mp4; codecs="mp4a.40.32"', a+'mp4; codecs="mp4a.40.33"', a+'mp4; codecs="mp4a.40.34"', a+'mp4; codecs="mp4a.40.35"', a+'mp4; codecs="mp4a.40.36"', a+'mp4; codecs="mp4a.40.42"', a+'mp4; codecs="mp4a.66"', a+'mp4; codecs="mp4a.67"', a+'mp4; codecs="mp4a.68"', a+'mp4; codecs="mp4a.69"', a+'mp4; codecs="mp4a.6B"', a+'mp4; codecs=".mp3"', ] let videolist = [ 'application/ogg', v+'3gpp', v+'3gpp2', v+'matroska', v+'mp4', v+'mp4; codecs=', v+'mp4; codecs=""', v+'mp4; codecs="avc1"', v+'mp4; codecs="avc3"', v+'mp4; codecs="flac"', v+'mp4; codecs="hev1.1.6.L93.B0"', // 1853448 v+'mp4; codecs="hev1.2.4.L120.B0"', v+'mp4; codecs="hvc1.1.6.L93.B0"', v+'mp4; codecs="hvc1.2.4.L120.B0"', v+'mp4; codecs="opus"', v+'mp4; codecs="vp09.00.10.08"', v+'mp4; codecs="vp09.00.50.08"', v+'mp4; codecs="vp09.00.51.08.01.01.01.01.00"', v+'mp4; codecs=\'\'', v+'ogg', v+'ogg; codecs="flac"', v+'ogg; codecs="opus"', v+'ogg; codecs="theora"', v+'ogg; codecs="theora, flac"', v+'ogg; codecs="theora, speex"', v+'ogg; codecs="theora, vorbis"', v+'quicktime', v+'webm', v+'webm; codecs="av1"', v+'webm; codecs="vorbis"', v+'webm; codecs="vp8"', v+'webm; codecs="vp8, opus"', v+'webm; codecs="vp8, vorbis"', v+'webm; codecs="vp9"', v+'webm; codecs="vp9, opus"', v+'webm; codecs="vp9, vorbis"', v+'x-m4v', v+'x-matroska', ] let videomp4 = [ "3gvo","a3d1","a3d2","a3d3","a3d4","a3ds","ac-3","ac-4","alac","alaw","av01","avc2","avc4","avcp","dra1", "drac","dts+","dts-","dtsc","dtse","dtsh","dtsl","dtsx","dvav","dvhe","ec-3","enca","encf","encm","encs", "enct","encv","fdp","g719","g726","hev1","hvc1","hvt1","ixse","lhv1","lhe1","lht1","m2ts","m4ae","mett", "metx","mha1","mha2","mhm1","mhm2","mjp2","mlix","mlpa","mp4a","mp4s","mp4v","mvc1","mvc2","mvc3","mvc4", "mvd1","mvd2","mvd3","mvd4","oksd","pm2t","prtp","raw","resv","rm2t","rrtp","rsrp","rtmd","rtp","s263", "samr","sawb","sawp","sevc","sm2t","sqcp","srtp","ssmv","stpp","stgs","svc1","svc2","svcm","tc64","tmcd", "twos","tx3g","ulaw","unid","urim","vc-1","vp08","vp09","wvtt", ] let videoavc1 = [ "42000a","42000b","42000c","42000d","420014","420015","420016","42001e","42001f","420020", "420028","420029","42002a","420032","420033","420034","42400a","42400b","42400c","42400d", "424014","424015","424016","42401e","42401f","424020","424028","424029","42402a","424032", "424033","424034","4d000a","4d000b","4d000c","4d000d","4d0014","4d0015","4d0016","4d001e", "4d001f","4d0020","4d0028","4d0029","4d002a","4d0032","4d0033","4d0034","4d400a","4d400b", "4d400c","4d400d","4d4014","4d4015","4d4016","4d401e","4d401f","4d4020","4d4028","4d4029", "4d402a","4d4032","4d4033","4d4034","58000a","58000b","58000c","58000d","580014","580015", "580016","58001e","58001f","580020","580028","580029","58002a","580032","580033","580034", "64000a","64000b","64000c","64000d","640014","640015","640016","64001e","64001f","640020", "640028","640029","64002a","640032","640033","640034","64080a","64080b","64080c","64080d", "640814","640815","640816","64081e","64081f","640820","640828","640829","64082a","640832", "640833","640834","6e000a","6e000b","6e000c","6e000d","6e0014","6e0015","6e0016","6e001e", "6e001f","6e0020","6e0028","6e0029","6e002a","6e0032","6e0033","6e0034","6e100a","6e100b", "6e100c","6e100d","6e1014","6e1015","6e1016","6e101e","6e101f","6e1020","6e1028","6e1029", "6e102a","6e1032","6e1033","6e1034","7a000a","7a000b","7a000c","7a000d","7a0014","7a0015", "7a0016","7a001e","7a001f","7a0020","7a0028","7a0029","7a002a","7a0032","7a0033","7a0034", "7a100a","7a100b","7a100c","7a100d","7a1014","7a1015","7a1016","7a101e","7a101f","7a1020", "7a1028","7a1029","7a102a","7a1032","7a1033","7a1034","f4000a","f4000b","f4000c","f4000d", "f40014","f40015","f40016","f4001e","f4001f","f40020","f40028","f40029","f4002a","f40032", "f40033","f40034","f4100a","f4100b","f4100c","f4100d","f41014","f41015","f41016","f4101e", "f4101f","f41020","f41028","f41029","f4102a","f41032","f41033","f41034","2c000a","2c000b", "2c000c","2c000d","2c0014","2c0015","2c0016","2c001e","2c001f","2c0020","2c0028","2c0029", "2c002a","2c0032","2c0033","2c0034", ] let a1 = [0, 1, 2], a2 = ["00","01","02","03","04","05","06","07","08","09",10,11,12,13,14,15,16,17,18,19,20,21,22,23,31], a3 = ["H","M"], a4 = ["08","10","12"] let videoav01 = [] a1.forEach(function(part1) { a2.forEach(function(part2) { a3.forEach(function(part3) { a4.forEach(function(part4) { let value = part1 +"."+ part2 +""+ part3 +"."+ part4 videoav01.push(value) }) }) }) }) videomp4.forEach(function(codec) {videolist.push('video/mp4; codecs="'+ codec +'"')}) videoavc1.forEach(function(codec) {videolist.push('video/mp4; codecs="avc1.'+ codec +'"')}) videoav01.forEach(function(codec) {videolist.push('video/mp4; codecs="av01.'+ codec +'"')}) let oData = {} function display(item) { let data = oData[item] let hash = mini(data) let count = "" if (item == "audiolist" || item == "videolist") {count = s13 +" ["+ data.length +"]"+ sc} dom.details.innerHTML = s13 + item + ": " +sc + hash + count +"<br>"+ json_highlight(data) //JSON.stringify(data, null, 2) } function getButton(name, text = "details") { return " <span class='btn13 btnc' onClick='display(`"+ name +"`)'>["+ text +"]</span>" } function get_media(type) { // https://privacycheck.sec.lrz.de/active/fp_cpt/fp_can_play_type.html // https://cconcolato.github.io/media-mime-support/ const METRICcan = "canPlayType_"+ type, METRICtype = "isTypeSupported_"+ type let oMedia = { "canPlay": {"maybe": [],"probably": []}, "isType": {"recorder": [],"source": []} } try { var obj = document.createElement(type) // collect let go1 = true, go2 = true, go3 = true, err1, err2, err3 let list = oData[type +"list"] list.sort() list.forEach(function(item) { let tmp = item.replace(type +"\/","") // strip "video/","audio/" if (go1) { try { let str = obj.canPlayType(item) if (str == "maybe" || str == "probably") {oMedia["canPlay"][str].push(tmp)} } catch(e) { go1 = false; err1 = zErr; console.log(type, "canPlayType", e) } } if (go2) { try { if (MediaRecorder.isTypeSupported(item)) {oMedia["isType"]["recorder"].push(tmp)} } catch(e) { go2 = false; err2 = zErr; console.log(type, "MediaRecorder", e) } } if (go3) { try { if (MediaSource.isTypeSupported(item)) {oMedia["isType"]["source"].push(tmp)} } catch(e) { go3 = false; err3 = zErr; console.log(type, "MediaSource", e) } } }) // canplay let canDisplay if (go1) { let aMaybe = oMedia["canPlay"]["maybe"] let aProbably = oMedia["canPlay"]["probably"] if (aMaybe.length == 0 && aProbably.length == 0) { canDisplay = "none" } else { let canobj = {} if (aMaybe.length) {canobj["maybe"] = aMaybe.sort()} if (aProbably.length) {canobj["probably"] = aProbably.sort()} let canHash = mini(canobj) canDisplay = canHash + getButton(METRICcan, aMaybe.length +"/" + aProbably.length) oData[METRICcan] = canobj } } else { canDisplay = err1 } dom[type +"can"].innerHTML = canDisplay // isType let typeDisplay if (!go2 && !go3) { typeDisplay = err2 // just display first error } else { let aRecorder = oMedia["isType"]["recorder"] let aSource = oMedia["isType"]["source"] if (aRecorder.length == 0 && aSource.length == 0) { typeDisplay = "none" } else { let typeobj = {} if (go2 && aRecorder.length) {typeobj["MediaRecorder"] = aRecorder.sort()} if (go3 && aSource.length) {typeobj["MediaSource"] = aSource.sort()} let typeHash = mini(typeobj) let notation = (go2 ? aRecorder.length : zErr) +"/"+ (go3 ? aSource.length : zErr) typeDisplay = typeHash + getButton(METRICtype, notation) oData[METRICtype] = typeobj } } dom[type +"type"].innerHTML = typeDisplay // ToDo: media: remove audio/video element? return } catch(e) { console.log(e) dom[type +"can"].innerHTML = zErr dom[type +"type"].innerHTML = zErr return } } function run() { // reset dom.details = "click counts to list their results here" oData = {} oData["audiolist"] = audiolist oData["videolist"] = videolist let t0 = performance.now() Promise.all([ get_media("audio"), get_media("video"), ]).then(function(){ dom.perf = Math.round(performance.now() - t0) + " ms" }) } // do once audiolist.sort() let audiocount = audiolist.length audiolist = audiolist.filter(function(item, position) {return audiolist.indexOf(item) === position}) if (audiolist.length !== audiocount) {console.log("audiolist has dupes")} videolist.sort() let videocount = videolist.length //console.log(videolist.join("\n")) videolist = videolist.filter(function(item, position) {return videolist.indexOf(item) === position}) if (videolist.length !== videocount) {console.log("videolist has dupes", videocount, videolist.length)} //console.log(videolist.join("\n")) run() </script> </body> </html> ================================================ FILE: tests/collation.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>collation</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2">collation</th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Output is limited to results that do not match English. Clicking run with no inputs will instead run examples</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <!-- localeCompare --> <hr><br> <span class="s4">LOCALECOMPARE</span> <span class="btn4 btn" onClick="compare()">[ run ]</span> <span class="btn4 btn" onClick="reset(`compare`)">[ clear input ]</span> <br><br> <input type="text" placeholder="a" id="valueA"> &nbsp; <input type="text" placeholder="A" id="valueB"> <br> <br><span class="spaces" id="output_compare"></span> <!-- Intl.Collator --> <br><br><hr><br> <span class="s4">INTL.COLLATOR</span> <span class="btn4 btn" onClick="collator()">[ run ]</span> <span class="btn4 btn" onClick="reset(`collator`)">[ clear input ]</span> <br><br> <textarea rows="3" placeholder="sort: comma delimited values: e.g: a,A,th,tw" style="width: 98%; resize: vertical" id="valueC"></textarea> <textarea rows="1" placeholder="search: comma delimited values: e.g: a,A,th,tw" style="width: 98%; resize: vertical" id="valueD"></textarea> <br><br> <span class="spaces" id="output_collator"></span> </td></tr> </table> <br> <script> 'use strict'; let isReverse = false var list = [] gLocales.forEach(function(str) { let code = str.split(",")[0] if (Intl.Collator.supportedLocalesOf([code]).length) {list.push(str)} }) var collatorSort = [ 'a', 'A', 'aa', 'ch', // cs,sk,sq,uz 'ez', 'kz', 'ng', 'ph', 'ts','tt', // ew, ha 'y', // latin small '\u00E2', // a + CIRCUMFLEX '\u00E4', // a + DIAERESIS '\u01FB', // a + RING ABOVE + ACUTE '\u0107', // c + ACUTE '\u0109', // c + CIRCUMFLEX '\u00E7\a', // c + CEDILLA '\u00EB', // e + DIAERESIS '\u00ED', // i + ACUTE '\u00EE', // i + CIRCUMFLEX '\u0137\a', // k + CEDILLA '\u0144', // n + ACUTE '\u00F1', // n + TILDE '\u1ED9', // o + CIRCUMFLEX + DOT BELOW '\u00F6', // o + DIAERESIS '\u1EE3', // o + HORN + DOT BELOW // other '\u0627', // ARABIC ALEF '\u0649', // ARABIC ALEF MAKSURA '\u06CC', // ARABIC FARSI YEH '\u06C6', // ARABIC OE '\u06C7', // ARABIC U '\u06FD', // ARABIC SINDHI AMPERSAND '\u0561', // ARMENIAN AYB small '\u09A4', // BENGALI TA '\u09CE', // BENGALI KHANDA TA '\u311A', // BOPOMOFO A '\u0453', // CYRILLIC GJE small '\uA647', // CYRILLIC IOTA small '\u0503', // CYRILLIC KOMI DJE small '\u0439', // CYRILLIC SHORT I small '\u0457', // CYRILLIC YI small '\u040E', // CYRILLIC SHORT U capital '\u04F0', // CYRILLIC U + DIAERESIS capital '\u4E2D', // CJK Ideograph '\u0934', // DEVANAGARI LLLA '\u0935', // DEVANAGARI VA '\u1208', // ETHIOPIC SYLLABLE LA (amharic) '\u10D0', // GEORGIAN AN '\u03B1', // GREEK ALPHA small '\u0A85', // GUJARATI A '\u3147', // HANGUL IEUNG '\u05EA', // HEBREW TAV '\uFB4A', // HEBREW TAV + DAGESH '\u0C85', // KANNADA A '\u1780', // KHMER KA '\u0E9A', // LAO BO '\u1D95', // LATIN SCHWA + RETROFLEX HOOK '\u025B', // LATIN SMALL OPEN E '\u0149', // LATIN SMALL N PRECEDED BY APOSTROPHE '\u00F0', // LATIN SMALL ETH small '\u1DD9', // COMBINING LATIN SMALL LETTER ETH '\u1820', // MONGOLIAN A '\u10350', // OLD PERMIC AN '\u0B05', // ORIYA A '\u0D85', // SINHALA AYANNA '\u0B85', // TAMIL A '\u0C05', // TELUGU A '\u0E24', // THAI RU ] var collatorSearch = [ // latin small '\u0107', // c + ACUTE '\u0109', // c + CIRCUMFLEX '\u1ED9', // o + CIRCUMFLEX + DOT BELOW '\u00F6', // o + DIAERESIS ] var compareExamples = [ '\u0109,\u00E7\a', 'a,\u03B1', '\u0107,\u0109', 'c,ch', '\u00EB,ez', '\u0137\a,kz', '\u1ED9,\u1EE3', '\u0109,ch', '\u0627,\u06FD', 'a,A', '\u00E7\a,ch', '\u0144,\u00F1', 'n,ng', '\u00ED,\u00EE', 'r,\u0453', '\u00F6,\u1EE3', '\u00F1,ng', '\u0107,ch', 'ts,tt', '\u1D95,\u025B', '\u040E,\u04F0', 'u,\u04F0', 'x,y', '\u05EA,\uFB4A', ] var aLegend = [], aLocales = [], aLocaleGroups, counter = 0, oSortvsSearch = {}, oRaw = {} function legend() { // build once if (aLegend.length == 0) { list.sort() let aCanonical = [] for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let test = Intl.Collator.supportedLocalesOf([code]) if (test.length == 1) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function reset(type) { if (type == "compare") { dom.valueA.value = "" dom.valueB.value = "" } else { dom.valueC.value = "" dom.valueD.value = "" } } function check_example() { let control = collatorSort.length // make sure example is dupe free collatorSort = collatorSort.filter(function (item, position) { return collatorSort.indexOf(item) === position }) if (collatorSort.length !== control) { console.debug("attention thorin: collation example contains dupes.. get your shit together") } } function compare() { function example() { valueA = compareExamples[counter].split(",")[0] valueB = compareExamples[counter].split(",")[1] counter++ if (counter == compareExamples.length) {counter = 0} } // clear let el = dom.output_compare // vars let valueA = dom.valueA.value, valueB = dom.valueB.value, isExample = false, go = false // make sure we have two valid values valueA = valueA.trim() valueB = valueB.trim() if (valueA.length && valueB.length) { if (valueA !== valueB) { dom.valueA.value = valueA dom.valueB.value = valueB go = true } } // run random example if (valueA.length == 0 && valueB.length == 0) { isExample = true example() go = true } if (!isExample) { el.innerHTML = "&nbsp" } // good to go if (go) { // delay so user can see changes setTimeout(function() { let output = [], str = "" if (isExample) { str = "example " + s12 + (counter == 0 ? compareExamples.length : counter) + sc +" of "+ s12 + compareExamples.length + sc +": comparing " + s12 + valueA + sc +" and "+ s12 + valueB + sc +"<br>" output.push(str) } else { str = "comparing " + s12 + valueA + sc +" and "+ s12 + valueB + sc +"<br>" output.push(str) } for (let i = 0 ; i < list.length; i++) { let control = valueA.localeCompare(valueB, "en-US") let code = list[i].split(",")[0] let name = list[i].split(",")[1].trim() let test = valueA.localeCompare(valueB, code) if (control !== test) { output.push(s12 + code.padStart(12) + sc +": "+ name) } } if (output.length == 1) { output.push(" nothing to report") } el.innerHTML = output.join("<br>") }, 170) } else { // crash and burn el.innerHTML = "please provide two different values" } } function collator() { // clear let el = dom.output_collator el.innerHTML = "&nbsp" legend() // vars let valueC = dom.valueC.value, valueD = dom.valueD.value, charsSort = [], charsSearch = [], goSort = false, goSearch = false // make sure we have at least two valid values valueC = valueC.trim() if (valueC.length) { let tmpArr = valueC.split(",") for (let i = 0 ; i < tmpArr.length; i++) { let trimmed = tmpArr[i].trim() if (trimmed.length) { charsSort.push(trimmed) } } // make sure we have more than one item charsSort = charsSort.filter(function (item, position) { return charsSort.indexOf(item) === position }) if (charsSort.length > 1) { goSort = true dom.valueC.value = charsSort.join(" , ") } } else { // use example charsSort = collatorSort goSort = true } valueD = valueD.trim() if (valueD.length) { let tmpArr = valueD.split(",") for (let i = 0 ; i < tmpArr.length; i++) { let trimmed = tmpArr[i].trim() if (trimmed.length) { charsSearch.push(trimmed) } } // make sure we have more than one item charsSearch = charsSearch.filter(function (item, position) { return charsSearch.indexOf(item) === position }) if (charsSearch.length > 1) { goSearch = true dom.valueD.value = charsSearch.join(" , ") } } else { // use example charsSearch = collatorSearch goSearch = true } // good to go if (goSort == true && goSearch == true) { // reset oSortvsSearch = {"search_is_nonenglish": [],} oRaw = {} // delay so user can see changes setTimeout(function() { let output = [] let isDetails = charsSort.join(', ').length > 50 // reset charsSort.sort() charsSearch.sort() // what if we reverse sorted, does that change anything if (isReverse) { //console.log('reversed chars arrays') charsSort.reverse() charsSearch.reverse() } //console.log(mini(charsSort), mini(charsSearch)) collatorSort.sort() collatorSearch.sort() let exampleHash = mini(collatorSort) +" | "+ mini(collatorSearch) let thisHash = mini(charsSort) +" | "+ mini(charsSearch) if (isReverse) { //console.log('reversed collator arrays') collatorSort.reverse() collatorSearch.reverse() exampleHash = mini(collatorSort) +" | "+ mini(collatorSearch) thisHash = mini(charsSort) +" | "+ mini(charsSearch) } // display chars used let strItem = s4.trim() +"ITEMS: "+ sc + charsSort.length + (thisHash == exampleHash ? s12 +" [example]"+ sc : "") +"<br><br>" + s12 +"sort: "+ mini(charsSort.join(" , ").trim()) +': '+ sc + charsSort.join(" , ").trim() +"<br><br>" + s12 +"search: "+ sc + charsSearch.join(" , ").trim() +"<br>" output.push(strItem) // set control charsSort = charsSort.sort(Intl.Collator("en").compare) let ensortdata = charsSort.join(" , ").trim() let ensorthash = mini(ensortdata) charsSearch.sort(Intl.Collator("en", {usage: "search"}).compare) let ensearchdata = charsSearch.join(" , ").trim() let ensearchhash = mini(ensearchdata) let entmpobj = { "search": ensearchdata, "sort": ensortdata, } let control = mini(entmpobj) if (undefined == oRaw[control]) {oRaw[control] = entmpobj} let control_display = s12 +'search: '+ sc + ensearchhash + s12 + " | sort: "+ sc + ensorthash + s12 + " | both: "+ sc + control strItem = s4.trim() +"CONTROL: "+ sc + sg + "en"+ sc +"<br><br>" + s12 +" hash: "+ sc + control_display + "<br>" let ctlDetails = "<li>"+ s14 +"search: "+ sc + oRaw[control]['search'] +"</li>" + "<li>"+ s14 +" sort: "+ sc + oRaw[control]['sort'] +"</li>" if (isDetails) {ctlDetails = "<li><details><summary>"+ s14 +"data"+ sc +"</summary><ul>"+ ctlDetails +"</ul></details></li>"} output.push(strItem +"<ul>"+ ctlDetails +"</ul>") // results header strItem = s4.trim() +"RESULTS"+ sc +"<br>" output.push(strItem) // loop let diffs = [], legendnew = [], codes = [] let oTempData = {} for (let i = 0 ; i < list.length; i++) { // important: reset to original order let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' // always reset order charsSearch.sort() charsSort.sort() if (isReverse) { //console.log('reversed test data', str) charsSearch.reverse() charsSort.reverse() } charsSearch.sort(Intl.Collator(code, {usage: "search"}).compare) let searchdata = charsSearch.join(" , ").trim() let searchhash = mini(searchdata) charsSort.sort(Intl.Collator(code, {usage: "sort"}).compare) let sortdata = charsSort.join(" , ").trim() let sorthash = mini(sortdata) let tmpobj = { "search": searchdata, "sort": sortdata, } let combinedhash = mini(tmpobj) if (undefined == oRaw[combinedhash]) {oRaw[combinedhash] = tmpobj} if (searchhash !== ensearchhash) { oSortvsSearch["search_is_nonenglish"].push(code + ": " + searchhash +", " + sorthash) } let hash = searchhash +" | "+ sorthash +" | "+ combinedhash // search then sort then combined if (combinedhash !== control) { codes.push(code) diffs.push(hash +":"+ code +":"+ name) if (oTempData[hash] == undefined) {oTempData[hash] = []} oTempData[hash].push(code +" "+ name) } let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } if (combinedhash == control) { legendnew.push(code.padStart(7) +": "+ name) } else { legendnew.push(sg + code.padStart(7) + sc +": "+ name) } } // we have diffs // sort by hash+locale so we can group hashes diffs.sort() if (diffs.length) { // output colored legend let header = s4 +"LEGEND ["+ list.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + legendnew.join("<br>") let tmpH = "", tmpC = "", tmpL = "", tmpStr = "", nxtH = "", locales = "" let newArr = [] for (let i = 0 ; i < diffs.length; i++) { // get current item tmpStr = diffs[i] tmpH = tmpStr.split(":")[0] // hash tmpC = tmpStr.split(":")[1] // code tmpL = tmpStr.split(":")[2] // locale name (assuming no more colons) locales += sg + tmpC + sc +" "+ tmpL +" " // color up code // grab next item if (i < diffs.length - 1) { nxtH = diffs[(i+1)].split(":")[0] } else { nxtH = "end" } // build new item if (nxtH !== tmpH) { newArr.push(locales.trim() +":"+ tmpH) locales = "" // reset } } // sort newArr (i.e order by first locale) newArr.sort() let lineItems = [] tmpH = "", tmpL = "", tmpStr = "" let len = 0, tmpCH = "" // newArr for (let i = 0 ; i < newArr.length; i++) { tmpStr = newArr[i] len = tmpStr.length tmpL = tmpStr.split(":")[0] tmpH = tmpStr.split(":")[1] // hashes tmpCH = tmpH.split(" | ")[2] // combined hash for data lookup tmpStr = s12 + (i + 1).toString().padStart(3) +": "+ sc // counter then hash then details then locales let strDetails = "<li>"+ s14 +"search: "+ sc + oRaw[tmpCH]['search'] +"</li>" + "<li>"+ s14 +" sort: "+ sc + oRaw[tmpCH]['sort'] +"</li>" if (isDetails) {strDetails = "<li><details><summary>"+ s14 +"data"+ sc +"</summary><ul>"+ strDetails +"</ul></details></li>"} lineItems.push (tmpStr + tmpH +"<br><ul>"+ strDetails +"<li>"+ tmpL +"</li></ul>") } // results output strItem = s12 +" stats: "+ sc + sg + newArr.length +" unique" + sc +" from "+ sg + codes.length + sc + " of "+ sg + aLocales.length + sc +" locales supported" output.push(strItem +"<br>") // locales hash aLocaleGroups = [] for (const k of Object.keys(oTempData)) { aLocaleGroups.push(oTempData[k]) } aLocaleGroups.sort() let localesHash = mini(aLocaleGroups) let ff = "" // don't notate unless it's our own preset if (isFF && thisHash == exampleHash) { // notate new if 140+ if (isVer > 139) { if (localesHash == "874da3f0") { // 1937541: FF150+ 83 } else if (localesHash == "bbe811d0") { // FF140+ 84 } else {localesHash += ' '+ zNEW} } } let localesBtn = " <span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" strItem = s12 +"locales: "+ sc + localesHash + localesBtn +"<br>" output.push(strItem) // details strItem = s4.trim() +"DETAILS"+ sc + s99 +' [search, sort, combined]' + sc +'<br>' output.push(strItem) output.push(lineItems.join("<br>")) } else { output.push("nothing to report") } el.innerHTML = output.join("<br>") }, 170) } else { // crash and burn el.innerHTML = "please provide two different values" } } function log_console(name) { if (name == 'locales') { let hash = mini(aLocaleGroups) console.log(name +': ' + hash +'\n'+ aLocaleGroups.join('\n')) } } check_example() Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // add additional locales to core locales for this test let aListExtra = [ "bs-cyrl,bosnian (cyrillic)", "fr-ca,french (canada)", // blink 'fa-af,persian (afghanistan)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() collator() }) </script> </body> </html> ================================================ FILE: tests/csscolors.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>css colors</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 480px; max-width: 580px;} </style> </head> <body> <div class="hidden"><span id="sColorElement"></span></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#css">return to TZP index</a></td></tr> </table> <table id="tb14"> <col width="15%"><col width="85%"> <thead><tr><th colspan="2"> <div class="nav-title">css colors <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"><span class="no_color"> <a target="_blank" class="blue" href="https://www.w3.org/TR/css-color-4/">https://www.w3.org/TR/css-color-4/</a> </span></td></tr> <tr> <td colspan="2" style="text-align: left;"> <hr><br> <span class="no_color c mono spaces" id="summary"></span> <br><br><hr><br> <span class="no_color c mono spaces" id="results"></span></td> </tr> </table> <br> <script> 'use strict'; let oColors = { "all": { "countcolor": 0, "countsupported": 0, "data": {}, "databyname": [], "hash": "", } } function get_colors() { /* https://www.w3.org/TR/css-color-4/ */ /* 95+: test_bug232227.html */ // sorted lists let oList = { "css4": [ '-moz-activehyperlinktext','-moz-default-color','-moz-default-background-color', '-moz-hyperlinktext','-moz-visitedhyperlinktext', 'AccentColor','AccentColorText','ActiveText','ButtonBorder','ButtonFace','ButtonText', 'Canvas','CanvasText','Field','FieldText','GrayText','Highlight','HighlightText','LinkText', 'Mark','MarkText','SelectedItem','SelectedItemText','VisitedText', ], "deprecated": [ // 23 "deprecated" 'ActiveBorder','ActiveCaption','AppWorkspace','Background','ButtonHighlight','ButtonShadow', 'CaptionText','InactiveBorder','InactiveCaption','InactiveCaptionText','InfoBackground', 'InfoText','Menu','MenuText','Scrollbar','ThreeDDarkShadow','ThreeDFace','ThreeDHighlight', 'ThreeDLightShadow','ThreeDShadow','Window','WindowFrame','WindowText', ], "moz": [ '-moz-cellhighlight','-moz-cellhighlighttext','-moz-combobox','-moz-comboboxtext','-moz-dialog', '-moz-dialogtext','-moz-field','-moz-fieldtext','-moz-html-cellhighlight','-moz-html-cellhighlighttext', '-moz-menubarhovertext','-moz-menuhover','-moz-menuhovertext','-moz-oddtreerow', // removed FF141: 1968925 '-moz-buttonhoverface','-moz-buttonhovertext', // removed FF140: can't find bugzilla '-moz-eventreerow', // removed FF122: 1867854 '-moz-mac-defaultbuttontext','-moz-mac-disabledtoolbartext', '-moz-mac-focusring','-moz-nativehyperlinktext', // removed FF121: 1863691 '-moz-mac-active-menuitem','-moz-mac-active-source-list-selection','-moz-mac-menuitem', '-moz-mac-menupopup','-moz-mac-source-list','-moz-mac-source-list-selection','-moz-mac-tooltip', // removed FF119: 1857695 '-moz-mac-menutextdisable','-moz-mac-menutextselect', // removed FF117 "-moz-buttondefault","-moz-dragtargetzone","-moz-mac-chrome-active","-moz-mac-chrome-inactive", "-moz-mac-menuselect","-moz-mac-menushadow","-moz-mac-secondaryhighlight","-moz-menubartext", "-moz-win-communicationstext","-moz-win-mediatext", // removed FF103 '-moz-mac-vibrant-titlebar-dark','-moz-mac-vibrant-titlebar-light', // removed FF94 '-moz-mac-buttonactivetext', // removed FF90 '-moz-gtk-info-bar-text', // removed FF88 '-moz-mac-vibrancy-dark','-moz-mac-vibrancy-light','-moz-win-accentcolor','-moz-win-accentcolortext', // removed FF78 or lower '-moz-accent-color','-moz-accent-color-foreground','-moz-appearance','-moz-colheaderhovertext', '-moz-colheadertext','-moz-gtk-buttonactivetext','-moz-mac-accentdarkestshadow', '-moz-mac-accentdarkshadow','-moz-mac-accentface','-moz-mac-accentlightesthighlight', '-moz-mac-accentlightshadow','-moz-mac-accentregularhighlight','-moz-mac-accentregularshadow', '-moz-win-communications-toolbox','-moz-win-media-toolbox', // test //'-moz-made-up' ], } // note: when contrast control (forced colors) is used, removed -moz named colors will be false positives function rgba2hex(orig) { var a, isPercent, rgb = orig.replace(/\s/g, '').match(/^rgba?\((\d+),(\d+),(\d+),?([^,\s)]+)?/i), alpha = (rgb && rgb[4] || "").trim(), hex = rgb ? (rgb[1] | 1 << 8).toString(16).slice(1) + (rgb[2] | 1 << 8).toString(16).slice(1) + (rgb[3] | 1 << 8).toString(16).slice(1) : orig; if (alpha !== '') {a = alpha } else { a = 0o1 rgb = rgb.slice(0, rgb.length - 1) } // multiply before convert to HEX a = ((a * 255) | 1 << 8).toString(16).slice(1) hex = hex + a rgb = rgb.slice(1, rgb.length) return hex +' '+ rgb.join('-') } try { const element = dom.sColorElement const strColor = "rgba(1, 2, 3, 0.5)" let tmpAll = {}, aTempAll = [], count = 0 for (const type of Object.keys(oList)) { let oTemp = {} let aList = oList[type] aList.sort() aList.forEach(function(style) { element.style.backgroundColor = strColor // reset color element.style.backgroundColor = style let rgb = window.getComputedStyle(element, null).getPropertyValue("background-color") if (rgb !== strColor) { // drop obsolete if (oTemp[rgb] == undefined) {oTemp[rgb] = [style]} else {oTemp[rgb].push(style)} } }) let tmpobj = {} count = 0 for (const k of Object.keys(oTemp)) {tmpobj[rgba2hex(k)] = oTemp[k]} // rgba2hex oColors[type] = { "countcolor": 0, "countsupported": 0, "data": {}, "databyname": [], "hash": "", } let aTemp = [] for (const k of Object.keys(tmpobj).sort()) { oColors[type]["data"][k] = tmpobj[k]; count += tmpobj[k].length // sort/count if (tmpAll[k] == undefined) {tmpAll[k] = []} tmpAll[k][type] = [] tmpAll[k][type] = tmpAll[k].concat(tmpobj[k]) tmpobj[k].forEach(function(item) { aTemp.push(item +": "+ k) }) // array by name } oColors[type]["countcolor"] = Object.keys(tmpobj).length oColors[type]["countsupported"] = count oColors[type]["hash"] = mini(oColors[type]["data"]) aTempAll = aTempAll.concat(aTemp) aTemp.sort() oColors[type]["databyname"] = aTemp } // all count = 0 aTempAll.sort() oColors["all"]["databyname"] = aTempAll for (const k of Object.keys(tmpAll).sort()) { oColors["all"]["data"][k] = {} for (const name of Object.keys(tmpAll[k]).sort()) { let data = tmpAll[k][name] // don't sort: leave in original groups oColors["all"]["data"][k][name] = data count += data.length } } oColors.all.countcolor += Object.keys(tmpAll).length oColors.all.countsupported += count oColors.all.hash = mini(oColors["all"]["data"]) output() } catch(e) { console.error(e.name, e.message) dom.summary = e.type +": "+ e.message } } function log_console(type, list) { let data = oColors[type][list], hash = mini(data), counts = "" if (list == "data") { counts = " ["+ oColors[type]["countcolor"] + "/" + oColors[type]["countsupported"] +"]" } console.log(type +": "+ hash + counts +"\n" + JSON.stringify(data, null, 2)) } function output() { let summary1 = [], summary2 = [], summary3 = [] for (const k of Object.keys(oColors).sort()) { let data = oColors[k] let str = k let counts = data.countcolor + "/" + data.countsupported counts = counts.padStart(6) str = "<span class='btn14 btnc' onClick='display(`" + k +"`)'>"+ str +"</span>: "+ counts summary1.push(str) let len = k.length + 8 summary2.push(s16 + (data.hash).padEnd(len) +sc) } dom.summary.innerHTML = summary1.join(" | ") +"<br>"+ summary2.join(" | ") display("all") } function display(type) { let results = [] let data = oColors[type]["data"], hash = oColors[type]["hash"], counts = " ["+ oColors[type]["countcolor"] + "/" + oColors[type]["countsupported"] +"]" let consoleBtns = " | CONSOLE: " + "<span class='btn14 btnc' onClick='log_console(`" + type +"`,`databyname`)'>[by name]</span> " + "<span class='btn14 btnc' onClick='log_console(`" + type +"`,`data`)'>[by color]</span> " results.push(s14 + type + sc + ": " + oColors[type]["hash"] + counts + consoleBtns +"<br><br>{") for (const k of Object.keys(data)) { let k1 = k.split(" ")[0] let k2 = k.split(" ")[1] results.push(" \""+ k1 +"\": <span style='background-color:#"+ k1 +"; border: 1px solid white'> &nbsp </span> "+ k2) let resultsStr = "" if (type == "all") { for (const name of Object.keys(data[k])) { resultsStr += "<ul>"+ s14 + name + sc +": " + data[k][name].join(", ") +"</ul>" } } else { resultsStr = "<ul>"+ data[k].join(", ") +"</ul>" } results.push(resultsStr) } dom.results.innerHTML = results.join("<br>") +"}" } get_colors() </script> </body> </html> ================================================ FILE: tests/dncalendar.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>dn: calendar</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="32%"><col width="68%"> <thead><tr><th colspan="2"> <div class="nav-title">displaynames: calendar <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames#script_code_display_names let aItems = [ //https://tc39.es/ecma402/#sec-calendar-types //https://github.com/unicode-org/cldr/blob/main/common/bcp47/calendar.xml // Ff140 counts 'gregory', // 150 'islamic-umalqura', // 111 | + 7 -> 162 'ethiopic', // 125 | + 3 -> 165 'chinese', // 124 | + 1 -> 166 'islamic-rgsa', // 39 | + 1 -> 167 - F147+ 1955545 Use "islamic-tbla" when either "islamic" or "islamic-rgsa" was requested 'roc', // 127 | + 1 -> 168 'buddhist', //125 'coptic', // 117 'dangi', // 118 'ethioaa', // 120 'hebrew', // 127 'indian', // 104 'islamic', // 110 - F147+ 1955545 Use "islamic-tbla" when either "islamic" or "islamic-rgsa" was requested 'islamic-civil', // 113 'islamic-tbla', // 55 'iso8601', // 122 'japanese', // 125 'persian', // 120 // other: is an alias //'ethiopic-amete-alem', // 120 135b2a92-max = same as ethioaa's hash ] // remove unsupported calendars let aGood = Intl.supportedValuesOf('calendar') aItems = aItems.filter(x => aGood.includes(x)) aItems.sort() var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = "" // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DisplayNames.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = '<br><br>' let tests = {} dom.perf = "" dom.results = "" if (method == "all") { tests = { 'calendar': {'long': aItems, 'narrow': aItems, 'short': aItems} } } else { // just one of the styles covers this, let's go with short tests = { 'calendar': { 'short': [ // FF147+ 'gregory', // 153 'islamic-umalqura', // 117 | + 12 -> 165 'ethiopic', // 129 | + 3 -> 168 'chinese', // 129 | + 1 -> 169 'roc', // 131 | + 1 -> 170 'islamic-tbla', // 59 | +1 -> 171 // these next two required FF147+ 'japanese', // 131 | +1 = 172 'dangi', // 123 | + 1 = 173 ] } } } try { aLocales.forEach(function(code) { // for each locale let oType = {} Object.keys(tests).sort().forEach(function(t){ // for each type oType[t] = {} for (const s of Object.keys(tests[t])) { // for each style oType[t][s] = ('all' == method) ? [] : {} let dn = new Intl.DisplayNames([code], {type: t, style: s}) tests[t][s].sort().forEach(function(item){ item = item.toLowerCase() let value = dn.of(item) if ('all' == method) { oType[t][s].push(item +': '+ value) } else { oType[t][s][item] = value } }) } }) let hash = mini(oType) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oType).forEach(function(typekey){ oTempData[hash][typekey] = oType[typekey] }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const key of Object.keys(oTempData[h]).sort()) { // for each granularity if (key == "locales") { oData[h][key] = oTempData[h][key].join(", ") } else { if (Object.keys(oTempData[h][key]).length) { oData[h][key] = oTempData[h][key] } } } } //console.log(oTempData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { // for each hash localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length let str = "" for (const type of Object.keys(oData[k])) { // for each type if (type !== "locales") { str += "<li>"+ s14 + type + sc +"</li>" Object.keys(oData[k][type]).forEach(function(s){ // for each style if ('all' == method) { // color up the parts let aParts = [], array = oData[k][type][s] array.forEach(function(item){ // for each item let parts = item.split(":") item = item.replace(parts[0] +':', s12 + parts[0]+ ':' + sc) aParts.push(item) }) str += s16 + s.slice(0,1).toUpperCase() +": "+ sc + aParts.join(', ') +"</br>" } else { str += s16 + s + sc + '<ul class="main">' Object.keys(oData[k][type][s]).forEach(function(item){ // for each item str += "<li>"+ s12 + item +': '+ sc + oData[k][type][s][item] +"</li>" }) str += '</ul>' } }) } } if ('all' == method) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "f9edfcf5") { // FF147+ } else if (resultsHash == "8010db57") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "c4ca9d63") { // FF147+: 173 } else if (localesHash == "8aee271e") { // FF140-146: 168 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { let test = new Intl.DisplayNames(undefined, {type: 'region'}).resolvedOptions().locale isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message +'banana' } // add additional locales to core locales for this test let aListExtra = [ 'bn-in,bengali (india)', 'bs-cyrl,bosnian (cyrillic)', 'en-au,english (australia)', 'en-ca,english (canada)', 'es-us,spanish (united states)', 'es-ar,spanish (argentina)', 'es-mx,spanish (mexico)', 'ff-adlm,fulah (adlam)', 'kk-cn,kazakh (china)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'pa-arab,punjabi (arabic)', 'pt-ao,portuguese (angola)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'sr-latn,serbian (latin)', 'sw-ke,swahili (kenya)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'yo-bj,yoruba (benin)', 'yue-cn,cantonese (china)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en','de','fr','pl','pt','sv'] legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dncurrency.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>dn: currency</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="32%"><col width="68%"> <thead><tr><th colspan="2"> <div class="nav-title">displaynames: currency <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy. The <code>TINY</code> test is a minimalist hardcoded test built for speed.</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <span id="btiny" class="btn4 btn" onClick="run('tiny')">[ TINY ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames#currency_code_display_names let aAll = [ // for reference 'ADP', 'AED', 'AFA', 'AFN', 'ALK', 'ALL', 'AMD', 'ANG', 'AOA', 'AOK', 'AON', 'AOR', 'ARA', 'ARL', 'ARM', 'ARP', 'ARS', 'ATS', 'AUD', 'AWG', 'AZM', 'AZN', 'BAD', 'BAM', 'BAN', 'BBD', 'BDT', 'BEC', 'BEF', 'BEL', 'BGL', 'BGM', 'BGN', 'BGO', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BOL', 'BOP', 'BOV', 'BRB', 'BRC', 'BRE', 'BRL', 'BRN', 'BRR', 'BRZ', 'BSD', 'BTN', 'BUK', 'BWP', 'BYB', 'BYN', 'BYR', 'BZD', 'CAD', 'CDF', 'CHE', 'CHF', 'CHW', 'CLE', 'CLF', 'CLP', 'CNH', 'CNX', 'CNY', 'COP', 'COU', 'CRC', 'CSD', 'CSK', 'CUC', 'CUP', 'CVE', 'CYP', 'CZK', 'DDM', 'DEM', 'DJF', 'DKK', 'DOP', 'DZD', 'ECS', 'ECV', 'EEK', 'EGP', 'ERN', 'ESA', 'ESB', 'ESP', 'ETB', 'EUR', 'FIM', 'FJD', 'FKP', 'FRF', 'GBP', 'GEK', 'GEL', 'GHC', 'GHS', 'GIP', 'GMD', 'GNF', 'GNS', 'GQE', 'GRD', 'GTQ', 'GWE', 'GWP', 'GYD', 'HKD', 'HNL', 'HRD', 'HRK', 'HTG', 'HUF', 'IDR', 'IEP', 'ILP', 'ILR', 'ILS', 'INR', 'IQD', 'IRR', 'ISJ', 'ISK', 'ITL', 'JMD', 'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRH', 'KRO', 'KRW', 'KWD', 'KYD', 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LTL', 'LTT', 'LUC', 'LUF', 'LUL', 'LVL', 'LVR', 'LYD', 'MAD', 'MAF', 'MCF', 'MDC', 'MDL', 'MGA', 'MGF', 'MKD', 'MKN', 'MLF', 'MMK', 'MNT', 'MOP', 'MRO', 'MRU', 'MTL', 'MTP', 'MUR', 'MVP', 'MVR', 'MWK', 'MXN', 'MXP', 'MXV', 'MYR', 'MZE', 'MZM', 'MZN', 'NAD', 'NGN', 'NIC', 'NIO', 'NLG', 'NOK', 'NPR', 'NZD', 'OMR', 'PAB', 'PEI', 'PEN', 'PES', 'PGK', 'PHP', 'PKR', 'PLN', 'PLZ', 'PTE', 'PYG', 'QAR', 'RHD', 'ROL', 'RON', 'RSD', 'RUB', 'RUR', 'RWF', 'SAR', 'SBD', 'SCR', 'SDD', 'SDG', 'SDP', 'SEK', 'SGD', 'SHP', 'SIT', 'SKK', 'SLE', 'SLL', 'SOS', 'SRD', 'SRG', 'SSP', 'STD', 'STN', 'SUR', 'SVC', 'SYP', 'SZL', 'THB', 'TJR', 'TJS', 'TMM', 'TMT', 'TND', 'TOP', 'TPE', 'TRL', 'TRY', 'TTD', 'TWD', 'TZS', 'UAH', 'UAK', 'UGS', 'UGX', 'USD', 'USN', 'USS', 'UYI', 'UYP', 'UYU', 'UYW', 'UZS', 'VEB', 'VED', 'VEF', 'VES', 'VND', 'VNN', 'VUV', 'WST', 'XAF', 'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XCD', 'XCG', 'XDR', 'XEU', 'XFO', 'XFU', 'XOF', 'XPD', 'XPF', 'XPT', 'XRE', 'XSU', 'XTS', 'XUA', 'XXX', 'YDD', 'YER', 'YUD', 'YUM', 'YUN', 'YUR', 'ZAL', 'ZAR', 'ZMK', 'ZMW', 'ZRN', 'ZRZ', 'ZWD', 'ZWG', 'ZWL', 'ZWR', ] // currencies: 307 supported in gecko | 159 in chrome // so we should filter these to those supported per engine let aItems = [] try {aAll = Intl.supportedValuesOf('currency')} catch(e) {} aAll.sort() aItems = aAll let aTestItems = [ // these add something in gecko but we can reduce the styles they test 'AFN', 'ANG', 'AOA', 'AUD', 'AWG', 'AZN', 'BAM', 'BBD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', 'BSD', 'BWP', 'BZD', 'CAD', 'CDF', 'CLP', 'CNY', 'COP', 'CRC', 'CUP', 'DJF', 'DKK', 'DOP', 'DZD', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'FRF', 'GBP', 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HNL', 'HTG', 'IDR', 'IQD', 'JMD', 'JPY', 'KES', 'KGS', 'KMF', 'KYD', 'KZT', 'LKR', 'LRD', 'LSL', 'LUF', 'MDL', 'MGA', 'MKD', 'MOP', 'MRU', 'MUR', 'MWK', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', 'NZD', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN', 'PTE', 'PYG', 'RUB', 'RUR', 'RWF', 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', 'SLE', 'SOS', 'SRD', 'SSP', 'STN', 'SYP', 'SZL', 'TND', 'TOP', 'TTD', 'TZS', 'UGX', 'USD', 'UYW', 'VES', 'VUV', 'WST', 'XAF', 'XCD', 'ZAR', 'ZMW', /* these add nothing in gecko or blink 'ADP', 'AED', 'AFA', 'ALK', 'ALL', 'AMD', 'AOK', 'AON', 'AOR', 'ARA', 'ARL', 'ARM', 'ARP', 'ARS', 'ATS', 'AZM', 'BAD', 'BAN', 'BDT', 'BEC', 'BEF', 'BEL', 'BGL', 'BGM', 'BGN', 'BGO', 'BHD', 'BOL', 'BOP', 'BOV', 'BRB', 'BRC', 'BRE', 'BRN', 'BRR', 'BRZ', 'BTN', 'BUK', 'BYB', 'BYN', 'BYR', 'CHE', 'CHF', 'CHW', 'CLE', 'CLF', 'CNH', 'CNX', 'COU', 'CSD', 'CSK', 'CUC', 'CVE', 'CYP', 'CZK', 'DDM', 'DEM', 'ECS', 'ECV', 'EEK', 'EGP', 'ESA', 'ESB', 'ESP', 'FIM', 'GEK', 'GEL', 'GHC', 'GNS', 'GQE', 'GRD', 'GWE', 'GWP', 'HKD', 'HRD', 'HRK', 'HUF', 'IEP', 'ILP', 'ILR', 'ILS', 'INR', 'IRR', 'ISJ', 'ISK', 'ITL', 'JOD', 'KHR', 'KPW', 'KRH', 'KRO', 'KRW', 'KWD', 'LAK', 'LBP', 'LTL', 'LTT', 'LUC', 'LUL', 'LVL', 'LVR', 'LYD', 'MAD', 'MAF', 'MCF', 'MDC', 'MGF', 'MKN', 'MLF', 'MMK', 'MNT', 'MRO', 'MTL', 'MTP', 'MVP', 'MVR', 'MXN', 'MXP', 'MXV', 'MZE', 'MZM', 'NIC', 'NLG', 'NOK', 'NPR', 'OMR', 'PEI', 'PES', 'PLZ', 'QAR', 'RHD', 'ROL', 'RON', 'RSD', 'SAR', 'SDD', 'SDP', 'SIT', 'SKK', 'SLL', 'SRG', 'STD', 'SUR', 'SVC', 'THB', 'TJR', 'TJS', 'TMM', 'TMT', 'TPE', 'TRL', 'TRY', 'TWD', 'UAH', 'UAK', 'UGS', 'USN', 'USS', 'UYI', 'UYP', 'UYU', 'UZS', 'VEB', 'VED', 'VEF', 'VND', 'VNN', 'XAG', 'XAU', 'XBA', 'XBB', 'XBC', 'XBD', 'XCG', 'XDR', 'XEU', 'XFO', 'XFU', 'XOF', 'XPD', 'XPF', 'XPT', 'XRE', 'XSU', 'XTS', 'XUA', 'XXX', 'YDD', 'YER', 'YUD', 'YUM', 'YUN', 'YUR', 'ZAL', 'ZMK', 'ZRN', 'ZRZ', 'ZWD', 'ZWG', 'ZWL', 'ZWR', //*/ ] //aItems = aTestItems // filter aItems aItems = aItems.filter(x => aAll.includes(x)) var list = gLocales, aLegend = [], aLocales = [], oTestData = {}, isSupported = false, localesHashAll = "" // to compare min to function compute_data() { aRes = [] for (const key of Object.keys(oTestData)) { if (key !== '1') { aRes.push(["'" + oTestData[key].join("','") +"', // "+ key]) } } console.log(aRes.join('\n')) } function testitems() { //loop each script on it's own dom.perf = "" dom.results = "" oTestData = {} try { aItems.forEach(function(item){ /* take one of the highest oTestData results and keep adding until you reach max item = [ 'VI','US','ZZ','CM','VC','TL','FR','QO','GB','ZA','KP','PL','KH','ZM', item ] //*/ // allow testing arrays of scripts let testarray = 'object' == typeof item ? item : [item] run_main('all', testarray, true) }) } catch(e) { console.log(e) } } function log_console(name) { let hash = mini(sDetail[name]) if (name == "currencies") { console.log(name +": " + sDetail[name].length +"\n"+ sDetail[name].join(", ")) } else if (name == "allcurrencies") { console.log("all supported currencies" +": " + aAll.length +"\n"+ aAll.join(", ")) } else if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DisplayNames.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } let oData = {} function run_main(method, aTest, isLoop = false) { let t0 = performance.now() oData = {} sDetail['currencies'] = [] let oTempData = {} let spacer = '<br><br>' let tests = {} let intTests = 0 let aUsed = undefined == aTest ? aItems: aTest if (!isLoop) { dom.perf = "" dom.results = "" } //aUsed = ['NZD','USD'] if (method == "all") { aUsed.sort() sDetail['currencies'] = aUsed // it is a lot faster to loop by style then currency tests = {'long': aUsed, 'narrow': aUsed, 'short': aUsed} intTests = aUsed.length * 3 // long 244 // narrow 168 // short 296 // combined 398 } else if (method == "min") { let minBase = [ 'AFN', 'ANG', 'AOA', 'AUD', 'AWG', 'AZN', 'BAM', 'BBD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', 'BSD', 'BWP', 'BZD', 'CAD', 'CDF', 'CLP', 'CNY', 'COP', 'CRC', 'CUP', 'DJF', 'DKK', 'DOP', 'DZD', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'FRF', 'GBP', 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HNL', 'HTG', 'IDR', 'IQD', 'JMD', 'JPY', 'KES', 'KGS', 'KMF', 'KYD', 'KZT', 'LKR', 'LRD', 'LSL', 'LUF', 'MDL', 'MGA', 'MKD', 'MOP', 'MRU', 'MUR', 'MWK', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', 'NZD', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN', 'PTE', 'PYG', 'RUB', 'RUR', 'RWF', 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', 'SLE', 'SOS', 'SRD', 'SSP', 'STN', 'SYP', 'SZL', 'TND', 'TOP', 'TTD', 'TZS', 'UGX', 'USD', 'UYW', 'VES', 'VUV', 'WST', 'XAF', 'XCD', 'ZAR', 'ZMW'] tests = { 'long': ['AUD','CAD','GBP','JPY','MZN','NIO','SEK','SZL','TZS','USD','XAF'], 'narrow': ['BND','EUR'], 'short': [ // long/narrow items: tested by removing them: all needed 'AUD', 'BND', 'CAD', 'EUR', 'GBP', 'JPY', 'MZN', 'NIO', 'SEK', 'SZL', 'TZS', 'USD', 'XAF', // the rest: required from earlier testing 'AFN', 'ANG', 'AOA', 'AWG', 'AZN', 'BAM', 'BBD', 'BIF', 'BMD', 'BOB', 'BRL', 'BSD', 'BWP', 'BZD', 'CDF', 'CLP', 'CNY', 'COP', 'CRC', 'CUP', 'DJF', 'DKK', 'DOP', 'DZD', 'ERN', 'ETB', 'FJD', 'FKP', 'FRF', 'GHS', 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HNL', 'HTG', 'IDR', 'IQD', 'JMD', 'KES', 'KGS', 'KMF', 'KYD', 'KZT', 'LKR', 'LRD', 'LSL', 'LUF', 'MDL', 'MGA', 'MKD', 'MOP', 'MRU', 'MUR', 'MWK', 'MYR', 'NAD', 'NGN', 'NZD', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN', 'PTE', 'PYG', 'RUB', 'RUR', 'RWF', 'SBD', 'SCR', 'SDG', 'SGD', 'SHP', 'SLE', 'SOS', 'SRD', 'SSP', 'STN', 'SYP', 'TND', 'TOP', 'TTD', 'UGX', 'UYW', 'VES', 'VUV', 'WST', 'XCD', 'ZAR', 'ZMW', ] } tests.short = tests.short.sort() } else { // TINY // we already use USD, GBP, ETB and KES in numberformat: currencydisplay // so ignore those tests = { // anything outside of the core longs is miniscule +1s or +2s 'long': [ 'JPY', // 177 'XAF', // 197 +20 'NIO', // 204 +7 'SZL', // 207 +3 'TZS', // 210 +3 'SEK', // 212 +2 //'AUD','CAD','MZN','GBP','USD', ], } } if ('all' !== method) { let tmpArray = [] for (const s of Object.keys(tests)) { tmpArray = tmpArray.concat(tests[s]) // remove unsupported tmpArray = tmpArray.filter(x => aAll.includes(x)) tests[s] = tmpArray intTests += tests[s].length } tmpArray = tmpArray.filter(function(item, position) {return tmpArray.indexOf(item) === position}) sDetail['currencies'] = tmpArray.sort() } //console.log(tests) try { aLocales.forEach(function(code) { // for each locale let oType = {} for (const s of Object.keys(tests).sort()) { // for each style oType[s] = {} let dn = new Intl.DisplayNames([code], {type: 'currency', style: s}) tests[s].sort().forEach(function(item){ let value = dn.of(item) oType[s][item] = value }) } let hash = mini(oType) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oType).forEach(function(typekey){ oTempData[hash][typekey] = oType[typekey] }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const key of Object.keys(oTempData[h]).sort()) { // for each granularity if (key == "locales") { oData[h][key] = oTempData[h][key].join(", ") } else { if (Object.keys(oTempData[h][key]).length) { oData[h][key] = oTempData[h][key] } } } } //console.log(oTempData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { // for each hash localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length if (!isLoop) { let str = "" for (const c of Object.keys(oData[k])) { // for each currency or style if (c !== "locales") { str += '<li>'+ s16 + c.slice(0,1).toUpperCase() +': '+ sc Object.keys(oData[k][c]).forEach(function(s){ // for each style or currency str += ' '+ s12 + s +": "+ sc + oData[k][c][s] }) str += "</li>" } } if ('tiny' !== method) {str = "<details><summary>data</summary>"+ str +"</details>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } } if (isLoop) { let testCount = localeGroups.length // record it if (undefined == oTestData[testCount]) {oTestData[testCount] = []} oTestData[testCount].push(aUsed) //console.log(testCount, aUsed) /* dom.results.innerHTML = dom.results.innerHTML + '<br>' + s16 + testCount + sc +': '+ '[\''+ aUsed.join('\', \'') +'\']' //*/ dom.results.innerHTML = dom.results.innerHTML + '<br>' + '\''+ aUsed.join('\', \'') +'\', // ' + testCount return } // hashes + btns let curBtn = "<span class='btn4 btnc' onClick='log_console(`allcurrencies`)'>[" + aAll.length + "]</span>" // all supported if (sDetail['currencies'].length !== aAll.length) { curBtn = "<span class='btn4 btnc' onClick='log_console(`currencies`)'>[" + sDetail.currencies.length + "]</span> from "+ curBtn } curBtn += ' ['+ intTests +' tests]' let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "c2325de7") { // FF147+ } else if (resultsHash == "3ada2b0a") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "295f8248") { // FF147+ 402 } else if (localesHash == "e5f55367") { // FF140-146 398 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s12 +"currencies: "+ sc + curBtn + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { let test = new Intl.DisplayNames(undefined, {type: 'region'}).resolvedOptions().locale isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message +'banana' } // add additional locales to core locales for this test let aListExtra = [ "af-na,afrikaans (namibia)", "ar-ae,arabic (united arabic emirates)", "ar-dj,arabic (djibouti)", "ar-er,arabic (eritrea)", "ar-km,arabic (cosmoros)", "ar-lb,arabic (lebanon)", "ar-so,arabic (somalia)", "ar-ss,arabic (south sudan)", "az-cyrl,azerbaijani (cyrillic)", "bn-in,bengali (india)", "bo-in,tibetan (india)", "bs-cyrl,bosnian (cyrillic)", "ca-fr,catalan (france)", "de-ch,german (switzerland)", "de-li,german (liechtenstein)", "de-lu,german (luxembourg)", "en-ag,english (antigua & barbuda)", "en-au,english (australia)", "en-bb,english (barbados)", "en-bi,english (burundi)", "en-bm,english (bermuda)", "en-bs,english (bahamas)", "en-bw,english (botswana)", "en-bz,english (belize)", "en-ca,english (canada)", "en-cc,english (cocos islands)", "en-dk,english (denmark)", "en-er,english (eritrea)", "en-fj,english (fiji)", "en-fk,english (falkland islands)", "en-gb,english (united kingdom)", "en-gg,english (guernsey)", "en-gh,english (ghana)", "en-gi,english (gibraltar)", "en-gm,english (gambia)", "en-gy,english (guyana)", "en-in,english (india)", "en-jm,english (jamaica)", "en-ke,english (kenya)", "en-ky,english (cayman islands)", "en-lr,english (liberia)", "en-ls,english (lesotho)", "en-mg,english (madagascar)", "en-mo,english (macau)", "en-mt,english (malta)", "en-mu,english (mauritius)", "en-mw,english (malawai)", "en-my,english (malaysia)", "en-na,english (namibia)", "en-ng,english (nigeria)", "en-nz,english (new zealand)", "en-pg,english (papua new guinea)", "en-pk,english (pakistan)", "en-rw,english (rwanda)", "en-sb,english (solomon islands)", "en-sc,english (seychelles)", "en-se,english (sweden)", "en-sg,english (singapore)", "en-sh,english (saint helena)", "en-sl,english (sierra leone)", "en-ss,english (south sudan)", "en-sx,english (sint maarten)", "en-sz,english (swaziland)", "en-to,english (tonga)", "en-tt,english (trinidad & tobago)", "en-tz,english (tanzania)", "en-ug,english (uganda)", "en-vu,english (vanuatu)", "en-ws,english (samoa)", "en-zm,english (zambia)", "es-419,spanish (latin america and the caribbean)", "es-ar,spanish (argentina)", "es-bo,spanish (bolivia)", "es-br,spanish (brazil)", "es-bz,spanish (belize)", "es-cl,spanish (chile)", "es-co,spanish (colombia)", "es-cr,spanish (costa rica)", "es-cu,spanish (cuba)", "es-do,spanish (dominican republic)", "es-ec,spanish (ecuador)", "es-gq,spanish (equatorial guinea)", "es-gt,spanish (guatemala)", "es-hn,spanish (honduras)", "es-mx,spanish (mexico)", "es-ni,spanish (nicaragua)", "es-pa,spanish (panama)", "es-pe,spanish (peru)", "es-ph,spanish (philippines)", "es-py,spanish (paraguay)", "es-us,spanish (united states)", "es-uy,spanish (uruguay)", "es-ve,spanish (venezuela)", "fa-af,persian (afghanistan)", "ff-adlm,fulah (adlam)", "ff-adlm-bf,fulah (adlam burkina faso)", "ff-adlm-gh,fulah (adlam ghana)", "ff-adlm-gm,fulah (adlam gambia)", "ff-adlm-lr,fulah (adlamd liberia)", "ff-adlm-mr,fulah (adlam mauritania)", "ff-adlm-ng,fulah (adlam nigeria)", "ff-adlm-sl,fulah (adlam sierra leone)", "ff-gn,fulah (guinea)", "ff-mr,fulah (mauritania)", "fo-dk,faroese (denmark)", "fr-bi,french (burundi)", "fr-ca,french (canada)", "fr-cd,french (congo kinshasa)", "fr-dj,french (djibouti)", "fr-dz,french (algeria)", "fr-gn,french (guinea)", "fr-ht,french (haiti)", "fr-km,french (comoros)", "fr-lu,french (luxembourg)", "fr-mg,french (madagascar)", "fr-mr,french (mauritania)", "fr-mu,french (mauritius)", "fr-rw,french (rwanda)", "fr-sc,french (seychelles)", "fr-sy,french (syria)", "fr-tn,french (tunisia)", "fr-vu,french (vanuatu)", "ha-gh,hausa (ghana)", "hi-latn,hindi (latin)", "hr-ba,croatian (bosnia & herzegovina)", 'kk-cn,kazakh (china)', "ks-deva,kashmiri (devanagari)", "kxv-telu,kuvi (telugu)", "ln-ao,lingala (angola)", "mas-tz,masia (tanzania)", "ms-bn,malay (brunei)", "ms-id,malay (indonesia)", "ms-sg,malay (singapore)", "nl-aw,dutch (aruba)", "nl-bq,dutch (caribbean netherlands)", "nl-cw,dutch (curaçao)", "nl-sr,dutch (suriname)", "om-ke,oromo (kenya)", "os-ru,ossetian (russia)", "pa-pk,punjabi (pakistan)", "ps-pk,pashto (pakistan)", "pt-ao,portuguese (angola)", "pt-cv,portuguese (cape verde)", "pt-lu,portuguese (luxembourg)", "pt-mo,portuguese (macau)", "pt-mz,portuguese (mazambique)", "pt-pt,portuguese (portugal)", "pt-st,portuguese (são tomé & príncipe)", "qu-bo,quechua (bolivia)", "qu-ec,quechua (ecuador)", "ro-md,romanian (moldova)", "ru-by,russian (belarus)", "ru-kg,russian (kyrgyzstan)", "ru-kz,russian (kazakhstan)", "ru-md,russian (moldova)", "sd-deva,sindhi (devanagari)", "se-se,northern sami", "shi-latn,tachelhit (latin)", "so-dj,somali (djibouti)", "so-et,somali (ethiopia)", "so-ke,somali (kenya)", "sq-mk,albanian (macedonia)", "sr-cyrl-ba,serbian (cyrillic bosnia & herzegovina)", "sr-latn,serbian (latin)", "sr-latn-ba,serbian (latin bosnia & herzegovina)", 'st-ls,southern sotho', "sw-cd,swahili (congo kinshasa)", "sw-ke,swahili (kenya)", "sw-ug,swahili (uganda)", "ta-lk,tamil (sri lanka)", "ta-my,tamil (malaysia)", "ta-sg,tamil (singapore)", "teo-ke,teso (kenya)", "ti-er,tigrinya (eritrea)", 'tn-bw,tswana (botswana)', "ur-in,urdu (india)", "uz-af,uzbek (afghanistan)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "vai-latn,vai (latin)", "yo-bj,yoruba (benin)", "yue-hans,cantonese (simplified)", "zh-hans-hk,chinese (simplified hong kong)", "zh-hans-mo,chinese (simplified macau)", "zh-hant-mo,chinese (traditional macau)", //*/ ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en','de','sv'] legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dndatetime.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>dn: datetimefield</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="32%"><col width="68%"> <thead><tr><th colspan="2"> <div class="nav-title">displaynames: datetimefield <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = "" // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DisplayNames.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" let tests = {} // https://tc39.es/ecma402/#table-validcodefordatetimefield let aItems = ['day','dayPeriod','era','hour','minute','month','quarter','second','timeZoneName','weekOfYear','weekday','year'] if (method == "all") { tests = { 'dateTimeField': { 'long': aItems, 'narrow': aItems, // 240 'short': aItems, }, } } else { // long doesn't add anything | hour, minute, month, quarter, and year also don't seem to add anything // narrow on it's own is only 1 unique item off tests = { 'dateTimeField': { 'narrow': ['day','dayPeriod','weekOfYear','weekday'], 'short': ['era','month','second','timeZoneName'], } } } try { aLocales.forEach(function(code) { // for each locale let oType = {} Object.keys(tests).sort().forEach(function(t){ // for each type oType[t] = {} for (const s of Object.keys(tests[t])) { // for each style oType[t][s] = ('all' == method) ? [] : {} let dn = new Intl.DisplayNames([code], {type: t, style: s}) tests[t][s].forEach(function(item){ let value = dn.of(item) if ('all' == method) { oType[t][s].push(item +': '+ value) } else { oType[t][s][item] = value } }) } }) let hash = mini(oType) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oType).forEach(function(typekey){ oTempData[hash][typekey] = oType[typekey] }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const key of Object.keys(oTempData[h]).sort()) { // for each granularity if (key == "locales") { oData[h][key] = oTempData[h][key].join(", ") } else { if (Object.keys(oTempData[h][key]).length) { oData[h][key] = oTempData[h][key] } } } } //console.log(oTempData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { // for each hash localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length let str = "" for (const type of Object.keys(oData[k])) { // for each type if (type !== "locales") { str += "<li>"+ s14 + type + sc +"</li>" Object.keys(oData[k][type]).forEach(function(s){ // for each style if ('all' == method) { // color up the parts let aParts = [], array = oData[k][type][s] array.forEach(function(item){ // for each item let parts = item.split(":") item = item.replace(parts[0] +':', s12 + parts[0]+ ':' + sc) aParts.push(item) }) str += s16 + s.slice(0,1).toUpperCase() +": "+ sc + aParts.join(', ') +"</br>" } else { str += s16 + s + sc + '<ul class="main">' Object.keys(oData[k][type][s]).forEach(function(item){ // for each item str += "<li>"+ s12 + item +': '+ sc + oData[k][type][s][item] +"</li>" }) str += '</ul>' } }) } } displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "c45bf84d") { // FF147+ } else if (resultsHash == "fc64a9af") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "f8cf35ab") { // FF147+ 245 } else if (localesHash == "4cd7e49a") { // FF140-146 241 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { let test = new Intl.DisplayNames(undefined, {type: 'region'}).resolvedOptions().locale isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message +'banana' } // add additional locales to core locales for this test let aListExtra = [ 'bs-cyrl,bosnian (cyrillic)', 'de-ch,german (switzerland)', 'en-001,english', 'en-ca,english (canada)', 'en-sg,english (singapore)', 'es-ar,spanish (argentina)', 'es-br,spanish (brazil)', 'es-do,spanish (dominican republic)', 'es-py,spanish (paraguay)', 'ff-adlm,fulah (adlam)', 'fr-ca,french (canada)', 'fr-ht,french (haiti)', 'hi-latn,hindi (latin)', 'kk-cn,kazakh (china)', 'kok-latn,konkani (latin)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'pa-arab,punjabi (arabic)', 'pt-ao,portuguese (angola)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'shi-latn,tachelhit (latin)', 'sr-ba,serbian (bosnia & herzegovina)', 'sr-latn,serbian (latin)', 'sr-latn-ba,serbian (latin bosnia & herzegovina)', 'sw-cd,swahili (congo kinshasa)', 'ur-in,urdu (india)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'vai-latn,vai (latin)', 'yo-bj,yoruba (benin)', 'yue-cn,cantonese (china)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en','de','fr','nl'] legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dnlanguage.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>dn: language</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="32%"><col width="68%"> <thead><tr><th colspan="2"> <div class="nav-title">displaynames: language <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy. The <code>TINY</code> test is a minimalist hardcoded test built for speed.</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <span id="btiny" class="btn4 btn" onClick="run('tiny')">[ TINY ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames#language_display_names /* item levels atm: default 298 likely 531 expanded 1090 locale levels atm: default 245 likely 495 expanded 1088 what is max unique? | likely | expand | <-- locales likely | 293 | 293 | expand | 293 | 293 | | items so that means we can safely use likely for aItems and then use default for locales and add extras (from usual suspects i.e likely) */ let aItemsIgnore = [] let aItems = [ // we rebuild this // these entries are for mmy notes on gecko entropy effects // and minimum test builds 'en', // 208 (199 core) 'fr-ch', // 236 (221 core) 'kl', // 242 (224 core) 'zh-hk', // 247 'gu', // 251 'bn-in', // 255 'sr-ba', // 258 // going up in twos 'bho','en-sb','gv','ko-kp','sw-cd', // 268 // going up in ones 'ar-eh','da','en-ae','en-ke','en-sh','ff', 'guz','ksh','kw','mgo', 'nb','nyn','pa-arab','raj','ro-md','rwk','saq','szl','te','twq', 'uz-arab','vmw','vun','zgh','zu', // end of gecko ] function setup_items(level = 'default') { // get data let aTmp = gLocalesOriginal if ('default' !== level) { aTmp = aTmp.concat(gLocalesLikely) } if ('expand' == level) { aTmp = aTmp.concat(gLocalesExpand) } aTmp = aTmp.filter(function(item, position) {return aTmp.indexOf(item) === position}) aTmp.sort() // clean up date aTmp.forEach(function(item){ item = item.split(',')[0].trim() item = item.toLowerCase() // only allow language and subtag: not region or variant if (item.split('-').length < 3) { aItems.push(item) } else { //console.log('rejected', item) } }) aItems.sort() } setup_items('likely') var list = gLocales, aLegend = [], aLocales = [], oTestData = {}, isSupported = false, localesHashAll = "" // to compare min to function testitems() { //loop each script on it's own dom.perf = "" dom.results = "" oTestData = {} try { aItems.forEach(function(item){ /* take one of the highest oTestData results and keep adding until you reach max item = [ 'en','fr-ch','kl','zh-hk','gu','bn-in','sr-ba', //258 // down to +2's 'bho','en-sb','gv','ko-kp','sw-cd', // 268 // down to +1s 'ar-eh','da','en-ae','en-ke','en-sh','ff', 'guz','ksh','kw','mgo', 'nb','nyn','pa-arab','raj','ro-md','rwk','saq','szl','te','twq', 'uz-arab','vmw','vun','zgh','zu', item ] //*/ // allow testing arrays of scripts let testarray = 'object' == typeof item ? item : [item] run_main('all', testarray, true) }) } catch(e) { console.log(e) } } function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DisplayNames.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method, aTest, isLoop = false) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = '<br><br>' let tests = {} let aUsed = undefined == aTest ? aItems: aTest if (!isLoop) { dom.perf = "" dom.results = "" } if (method == "all") { aUsed.sort() //tests = {'language': {'dialect': aUsed, 'standard': aUsed}} // temp just use dialect as we work out min items tests = {'language': {'dialect': aUsed}} } else if (method == "min") { tests = { 'language': { 'dialect': [ 'en','fr-ch','kl','zh-hk','gu','bn-in','sr-ba', //258 // down to +2's 'bho','en-sb','gv','ko-kp','sw-cd', // 268 // down to +1s 'ar-eh','da','en-ae','en-ke','en-sh','ff', 'guz','ksh','kw','mgo', 'nb','nyn','pa-arab','raj','ro-md','rwk','saq','szl','te','twq', 'uz-arab','vmw','vun','zgh','zu', ], // and blink needs help 'standard': [ 'ak','az','be','bs','haw', ], } } } else { // TINY tests = { 'language': { 'dialect': [ 'en','fr-ch','kl','zh-hk', 'gu', // splits include en-us, various spanish/french 'bn-in', // splits include en-au, en-in 'sr-ba', // splits include en-ca, es-br ] } } } try { aLocales.forEach(function(code) { // for each locale let oType = {} Object.keys(tests).sort().forEach(function(t){ // for each type oType[t] = {} for (const s of Object.keys(tests[t])) { // for each style oType[t][s] = ('all' == method) ? [] : {} let dn = new Intl.DisplayNames([code], {type: t, languageDisplay: s}) tests[t][s].sort().forEach(function(item){ let value = dn.of(item) if ('all' == method) { oType[t][s].push(item +': '+ value) } else { oType[t][s][item] = value } }) } }) let hash = mini(oType) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oType).forEach(function(typekey){ oTempData[hash][typekey] = oType[typekey] }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const key of Object.keys(oTempData[h]).sort()) { // for each granularity if (key == "locales") { oData[h][key] = oTempData[h][key].join(", ") } else { if (Object.keys(oTempData[h][key]).length) { oData[h][key] = oTempData[h][key] } } } } //console.log(oTempData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { // for each hash localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length if (!isLoop) { let str = "" for (const type of Object.keys(oData[k])) { // for each type if (type !== "locales") { str += "<li>"+ s14 + type + sc +"</li>" Object.keys(oData[k][type]).forEach(function(s){ // for each style if ('all' == method) { // color up the parts let aParts = [], array = oData[k][type][s] array.forEach(function(item){ // for each item let parts = item.split(":") item = item.replace(parts[0] +':', s12 + parts[0]+ ':' + sc) aParts.push(item) }) str += s16 + s +": "+ sc + aParts.join(', ') +"</br>" } else { str += s16 + s + sc + '<ul class="main">' Object.keys(oData[k][type][s]).forEach(function(item){ // for each item str += "<li>"+ s12 + item +': '+ sc + oData[k][type][s][item] +"</li>" }) str += '</ul>' } }) } } if ('tiny' !== method) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } } if (isLoop) { let testCount = localeGroups.length // record it if (undefined == oTestData[testCount]) {oTestData[testCount] = []} oTestData[testCount].push(aUsed) //console.log(testCount, aUsed) /* dom.results.innerHTML = dom.results.innerHTML + '<br>' + s16 + testCount + sc +': '+ '[\''+ aUsed.join('\', \'') +'\']' //*/ dom.results.innerHTML = dom.results.innerHTML + '<br>' + '\''+ aUsed.join('\', \'') +'\', // ' + testCount return } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "5dc44060") { // FF147+ } else if (resultsHash == "6da69df6") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "86d81065") { // FF147+ 293 } else if (localesHash == "95e2270c") { // FF140-146 293 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { let test = new Intl.DisplayNames(undefined, {type: 'region'}).resolvedOptions().locale isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message +'banana' } // add additional locales to core locales for this test let aListExtra = [ //* 'ar-eg,arabic (egypt)', 'ar-ly,arabic (libya)', 'ar-sa,arabic (saudi arabia)', 'az-cyrl,azerbaijani (cyrillic)', 'bn-in,bengali (india)', 'bs-cyrl,bosnian (cyrillic)', 'de-at,german (austria)', 'de-ch,german (switzerland)', 'en-ag,english (antigua & barbuda)', 'en-au,english (australia)', 'en-ca,english (canada)', 'en-gb,english (united kingdom)', 'en-in,english (india)', 'es-ar,spanish (argentina)', 'es-br,spanish (brazil)', 'es-cl,spanish (chile)', 'es-mx,spanish (mexico)', 'es-pr,spanish (puerto rico)', 'es-us,spanish (united states)', 'fa-af,persian (afghanistan)', 'ff-adlm,fulah (adlam)', 'fr-be,french (belgium)', 'fr-ca,french (canada)', 'fr-ch,french (switzerland)', 'hi-latn,hindi (latin)', 'kk-cn,kazakh (china)', 'ko-kp,korean (north korea)', 'kok-latn,konkani (latin)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'pa-arab,punjabi (arabic)', 'ps-pk,pashto (pakistan)', 'pt-ao,portuguese (angola)', 'ro-md,romanian (moldova)', 'ru-ua,russian (ukraine)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'shi-latn,tachelhit (latin)', 'sr-ba,serbian (bosnia & herzegovina)', 'sr-cyrl-me,serbian (cyrillic montenegro)', 'sr-cyrl-xk,serbian (cyrillic kosovo)', 'sr-latn,serbian (latin)', 'sr-latn-ba,serbian (latin bosnia & herzegovina)', 'sr-latn-me,serbian (latin montenegro)', 'sr-latn-xk,serbian (latin kosovo)', 'sw-cd,swahili (congo kinshasa)', 'sw-ke,swahili (kenya)', 'ti-er,tigrinya (eritrea)', 'ur-in,urdu (india)', 'uz-af,uzbek (afghanistan)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'vai-latn,vai (latin)', 'yo-bj,yoruba (benin)', 'yue-cn,cantonese (china)', //*/ ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en','de','fr','pl','pt','sv'] legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dnregion.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>dn: region</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="32%"><col width="68%"> <thead><tr><th colspan="2"> <div class="nav-title">displaynames: region <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy. The <code>TINY</code> test is a minimalist hardcoded test built for speed.</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <span id="btiny" class="btn4 btn" onClick="run('tiny')">[ TINY ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames#region_code_display_names // ISO-3166 2-letter country codes // https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 // https://www.iso.org/obp/ui/#iso:pub:PUB500001:en /** 0. ensure a good range of locales in list 1. generate_codes() -> all possible 676 two-letter codes into aItems 2. testregions() test each code individually and populate into oTestData where codes are bucketized by their unique hash count 3. oTestData e.g. oTestData[1] shows 385 codes, such as 'AA' e.g. console.log("'" + oTestData[1].join("','") +"'") these are the useless codes run_main('all',aItemsIgnore) and see for yourself 4. compute_data() ignore oTestData[1] logs to console an output for pasting into aRegions as a starting point gecko 140+ code space: 676 | not supported 385 | left for max tests 291 **/ let aItemsIgnore = [ // 385 confirmed all locales return the same hash // run_main('all', aItemsIgnore) 'AA','AB','AH','AJ','AK','AP','AV','AY','BC','BK','BP','BX','CB','CE','CJ','DA','DB','DC','DF','DH', 'DI','DL','DN','DP','DQ','DR','DS','DT','DU','DV','DW','DX','EB','ED','EF','EI','EJ','EK','EL','EM', 'EN','EO','EP','EQ','EV','EW','EX','EY','FA','FB','FC','FD','FE','FF','FG','FH','FL','FN','FP','FS', 'FT','FU','FV','FW','FY','FZ','GC','GJ','GK','GO','GV','GX','GZ','HA','HB','HC','HD','HE','HF','HG', 'HH','HI','HJ','HL','HO','HP','HQ','HS','HW','HX','HY','HZ','IA','IB','IF','IG','IH','II','IJ','IK', 'IP','IU','IV','IW','IX','IY','IZ','JA','JB','JC','JD','JF','JG','JH','JI','JJ','JK','JL','JN','JQ', 'JR','JS','JU','JV','JW','JX','JY','JZ','KA','KB','KC','KD','KF','KJ','KK','KL','KO','KQ','KS','KT', 'KU','KV','KX','LD','LE','LF','LG','LH','LJ','LL','LM','LN','LO','LP','LQ','LW','LX','LZ','MB','MJ', 'NB','ND','NJ','NK','NM','NN','NS','NV','NW','NX','NY','OA','OB','OC','OD','OE','OF','OG','OH','OI', 'OJ','OK','OL','ON','OO','OP','OQ','OR','OS','OT','OU','OV','OW','OX','OY','OZ','PB','PD','PI','PJ', 'PO','PP','PQ','PV','PX','QB','QC','QD','QE','QF','QG','QH','QI','QJ','QK','QL','QM','QN','QP','QQ', 'QR','QS','QT','QV','QW','QX','QY','QZ','RA','RB','RC','RD','RF','RG','RI','RJ','RK','RL','RM','RN', 'RP','RQ','RR','RT','RV','RX','RY','RZ','SF','SP','SQ','SW','TB','TE','TI','TQ','TS','TU','TX','TY', 'UB','UC','UD','UE','UF','UH','UI','UJ','UL','UO','UP','UQ','UR','UT','UU','UV','UW','UX','VB','VF', 'VH','VJ','VK','VL','VM','VO','VP','VQ','VR','VS','VT','VV','VW','VX','VY','VZ','WA','WB','WC','WD', 'WE','WG','WH','WI','WJ','WL','WM','WN','WO','WP','WQ','WR','WT','WU','WV','WW','WX','WY','WZ','XC', 'XD','XE','XF','XG','XH','XI','XJ','XL','XM','XN','XO','XP','XQ','XR','XS','XT','XU','XV','XW','XX', 'XY','XZ','YA','YB','YC','YF','YG','YH','YI','YJ','YK','YL','YM','YN','YO','YP','YQ','YR','YS','YV', 'YW','YX','YY','YZ','ZB','ZC','ZD','ZE','ZF','ZG','ZH','ZI','ZJ','ZK','ZL','ZN','ZO','ZP','ZQ','ZS', 'ZT','ZU','ZV','ZX','ZY', ] // 291 confirmed supported in FF140+ let aItems = [ // first cover gLocales: alpha-2 and alpha-3 codes (aiming for 233/245) 'VI', // 185 'US', // 184 'ZZ', // 150 'CM', // 106 and after that we're growing in 2's // switch to extended locale list // starting at 229 for the above: aiming for 281 'VC', // 179 | 235 'TL', // 139 | 239 'FR', // 149 | 242 'QO', // 144 | 245 'GB', // 183 | 247 now growing in 2's 'ZA', // 169 | 249 'KP', // 167 | 251 'PL', // 129 | 253 'KH', // 124 | 255 'ZM', // 101 | 257 // now growing in 1's 'UK','VG', // 182 'PS', // 181 'TC', // 181 'MP', // 179 'KY', // 178 'CK','MH', // 176 'BA', // 175 'NF', // 175 'SB', // 174 'CF','FK', // 173 'PM', // 171 'GF','PF', // 170 'AE', // 169 'KR', // 167 'DO','ST', // 166 'GQ','KN', // 165 'AS','PN', // 161 'NZ', // 160 'CH','PG', // 158 'CZ','DD','DE','NC', // 157 'GS', // 155 'AG', // 153 'NT','SA','TT', // 152 'JT', // 151 'MI','PU','UM','WK', // 151 'GR','HM','TF', // 150 'FX', // 149 'AZ','CC','NL', // 148 'CD','MO','ZR', // 147 'CI','WF', // 146 'IC', // 145 'CX','HK','RU','SC','SU','TR', // 144 'HR', // 143 'ES','FO', // 142 'AT','KM','VA', // 141 'AX','SH', // 140 'MV','NO', // 139 'TP', // 139 'BV','CV','HU', // 138 'GE','SE', // 137 'EG', // 136 'BE','DZ','EU','QU', // 135 'CY','KG','PH', // 134 'CN','CP','GW', // 133 'FM','PC', // 132 'EH','LC', // 131 'AC','AU','BG','BQ','GL','UN', // 130 'RO','SJ','SK', // 129 'CG','IE', // 128 'IS', // 128 'IM','JO','SI', // 128 'BR','BU','MM','UZ', // 127 'DK','ET','MR','MU','SS', // 126 'AM','FI','JP','TH','TM', // 125 'BY', // 124 'KZ','LU','MK', // 123 'IN','TJ', // 122 'NG','SL','UA', // 120 'AR','ID','IL','PT','XA', // 119 'SZ', // 118 'BD','EE','LT','RE', // 117 'EA','ER','GP','IT','LI','MY','VD','VN', // 116 'AL','DJ','TN', // 115 'MD', // 114 'BH','BO','CL','IO','MX', // 113 'BM', // 112 'AF','CO','LV','PR','SV','VE', // 111 'MN','SY', // 110 'BN','BS','GI','TD','XB', // 109 'EZ','UY', // 108 'BF','HV','MS','NE', // 107 'EC','MZ', // 106 'AI','MA','PY', // 105 'BL','CR','LR','SO','TZ', // 104 'GD','JM', // 103 'HN','KW','RH','SM','ZW', // 102 'BW','LB','SD','YT', // 101 // under 100 'AD','AN','AO','AQ','AW','BB','BI','BJ', 'BT','BZ','CA','CQ','CS','CT','CU','CW', 'DG','DM','DY','FJ','FQ','GA','GG','GH', 'GM','GN','GT','GU','GY','HT','IQ','IR', 'JE','KE','KI','LA','LK','LS','LY','MC', 'ME','MF','MG','ML','MQ','MT','MW','NA', 'NH','NI','NP','NQ','NR','NU','OM','PA', 'PE','PK','PW','PZ','QA','RS','RW','SG', 'SN','SR','SX','TA','TG','TK','TO','TV', 'TW','UG','VU','WS','XK','YD','YE','YU', ] aItems.sort() function generate_codes() { let aAll = [] for (let i =65; i < 91; i++) { let one = String.fromCharCode(i) for (let j =65; j < 91; j++) { let two = String.fromCharCode(j) aAll.push(one + two) } } console.log(aAll) aRegions = aAll } function compute_data() { aRes = [] for (const key of Object.keys(oTestData)) { if (key !== '1') { aRes.push(["'" + oTestData[key].join("','") +"', // "+ key]) } } console.log(aRes.join('\n')) } var list = gLocales, aLegend = [], aLocales = [], oTestData = {}, isSupported = false, localesHashAll = "" // to compare min to function testitems() { //loop each script on it's own dom.perf = "" dom.results = "" oTestData = {} try { aItems.forEach(function(item){ /* take one of the highest oTestData results and keep adding until you reach max item = [ 'VI','US','ZZ','CM','VC','TL','FR','QO','GB','ZA','KP','PL','KH','ZM', item ] //*/ // allow testing arrays of scripts let testarray = 'object' == typeof item ? item : [item] run_main('all', testarray, true) }) } catch(e) { console.log(e) } } function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DisplayNames.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method, aTest, isLoop = false) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = '<br><br>' let tests = {} let aUsed = undefined == aTest ? aItems: aTest if (!isLoop) { dom.perf = "" dom.results = "" } if (method == "all") { aUsed.sort() tests = {'region': {'long': aUsed, 'narrow': aUsed, 'short': aUsed}} } else if (method == "min") { tests = { 'region': { 'narrow': [ 'VI','US','ZZ','CM','VC','TL','FR','QO','GB','ZA','KP','PL','KH','ZM', // in ones 'AS','CN','EH','GH','GL','GS','IM','IT','JT','KE','KY','LS', 'MM','MO','MS','MZ','NL','PK','PS','RW','SJ','SS','SZ','TF', 'UM', // and UM for blink ], } } } else { // TINY tests = { 'region': {'narrow': ['VI','US','ZZ','CM','VC','TL','FR'],} } } try { aLocales.forEach(function(code) { // for each locale let oType = {} Object.keys(tests).sort().forEach(function(t){ // for each type oType[t] = {} for (const s of Object.keys(tests[t])) { // for each style oType[t][s] = ('tiny' !== method) ? [] : {} let dn = new Intl.DisplayNames([code], {type: t, style: s}) tests[t][s].sort().forEach(function(item){ let value = dn.of(item) if ('tiny' !== method) { oType[t][s].push(item +': '+ value) } else { oType[t][s][item] = value } }) } }) let hash = mini(oType) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oType).forEach(function(typekey){ oTempData[hash][typekey] = oType[typekey] }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const key of Object.keys(oTempData[h]).sort()) { // for each granularity if (key == "locales") { oData[h][key] = oTempData[h][key].join(", ") } else { if (Object.keys(oTempData[h][key]).length) { oData[h][key] = oTempData[h][key] } } } } //console.log(oTempData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { // for each hash localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length if (!isLoop) { let str = "" for (const type of Object.keys(oData[k])) { // for each type if (type !== "locales") { str += "<li>"+ s14 + type + sc +"</li>" Object.keys(oData[k][type]).forEach(function(s){ // for each style if ('tiny' !== method) { // color up the parts let aParts = [], array = oData[k][type][s] array.forEach(function(item){ // for each item let parts = item.split(":") item = item.replace(parts[0] +':', s12 + parts[0]+ ':' + sc) aParts.push(item) }) str += s16 + s.slice(0,1).toUpperCase() +": "+ sc + aParts.join(', ') +"</br>" } else { str += s16 + s + sc + '<ul class="main">' Object.keys(oData[k][type][s]).forEach(function(item){ // for each item str += "<li>"+ s12 + item +': '+ sc + oData[k][type][s][item] +"</li>" }) str += '</ul>' } }) } } if ('tiny' !== method) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } } if (isLoop) { let testCount = localeGroups.length // record it if (undefined == oTestData[testCount]) {oTestData[testCount] = []} oTestData[testCount].push(aUsed) //console.log(testCount, aUsed) /* dom.results.innerHTML = dom.results.innerHTML + '<br>' + s16 + testCount + sc +': '+ '[\''+ aUsed.join('\', \'') +'\']' //*/ dom.results.innerHTML = dom.results.innerHTML + '<br>' + '\''+ aUsed.join('\', \'') +'\', // ' + testCount return } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "7b25db13") { // FF147+ } else if (resultsHash == "a0c62bf7") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "57c71705") { // FF147+ 283 } else if (localesHash == "3207fd4a") { // FF140-146 281 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { let test = new Intl.DisplayNames(undefined, {type: 'region'}).resolvedOptions().locale isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message +'banana' } // add additional locales to core locales for this test let aListExtra = [ 'ar-ly,arabic (libya)', 'ar-sa,arabic (saudi arabia)', 'az-cyrl,azerbaijani (cyrillic)', 'bn-in,bengali (india)', 'bs-cyrl,bosnian (cyrillic)', 'de-at,german (austria)', 'de-ch,german (switzerland)', 'en-au,english (australia)', 'en-ca,english (canada)', 'en-in,english (india)', 'es-ar,spanish (argentina)', 'es-br,spanish (brazil)', 'es-cl,spanish (chile)', 'es-mx,spanish (mexico)', 'es-pr,spanish (puerto rico)', 'es-us,spanish (united states)', 'fa-af,persian (afghanistan)', 'ff-adlm,fulah (adlam)', 'fr-be,french (belgium)', 'fr-ca,french (canada)', 'hi-latn,hindi (latin)', 'kk-cn,kazakh (china)', 'ko-kp,korean (north korea)', 'kok-latn,konkani (latin)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'pa-arab,punjabi (arabic)', 'ps-pk,pashto (pakistan)', 'pt-ao,portuguese (angola)', 'ro-md,romanian (moldova)', 'ru-ua,russian (ukraine)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'shi-latn,tachelhit (latin)', 'sr-ba,serbian (bosnia & herzegovina)', 'sr-cyrl-me,serbian (cyrillic montenegro)', 'sr-cyrl-xk,serbian (cyrillic kosovo)', 'sr-latn,serbian (latin)', 'sr-latn-ba,serbian (latin bosnia & herzegovina)', 'sr-latn-me,serbian (latin montenegro)', 'sr-latn-xk,serbian (latin kosovo)', 'sw-cd,swahili (congo kinshasa)', 'sw-ke,swahili (kenya)', 'ur-in,urdu (india)', 'uz-af,uzbek (afghanistan)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'vai-latn,vai (latin)', 'yo-bj,yoruba (benin)', 'yue-cn,cantonese (china)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en','de','fr','pl','pt','sv'] legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dnscript.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>dn: script</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="32%"><col width="68%"> <thead><tr><th colspan="2"> <div class="nav-title">displaynames: script <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames#script_code_display_names // https://unicode.org/iso15924/iso15924-codes.html // nonsense 4 letter codes, or ones not supported simply // return themselves in all styles: we can determine these // one by one if we get a single unique hash across all locales let aItemsIgnore = [ // confirmed all locales return the same hash 'Berf','Chis','Hntl','Kitl','Leke','Nkdb','Pcun','Pelm','Piqd', 'Psin','Qaaa','Qabx','Ranj','Seal','Shui','Sidt','Tayo','Tols', ] // confirmed supported in FF140+ let aItems = [ 'Zxxx', // 155 'Latn', // 127 -> 174 'Hans', // 150 -> 181 'Arab', // 124 -> 183 'Deva', // 89 -> 185 'Zzzz', // 152 -> 187 'Cyrl', // 128 -> 188 'Mymr', // 109 -> 189 'Mong', // 108 -> 190 'Beng', // 106 -> 191 'Orya', // 92 -> 192 'Guru', // 90 -> 193 'Hant', // 145 'Hrkt', // 125 'Ethi', // 124 'Jpan', // 122 'Armn', // 119 'Cans', // 119 'Geor', // 117 'Hebr', // 117 'Kore', // 116 'Zyyy', // 114 'Grek', // 113 'Zmth', // 112 'Zsym', // 111 'Sinh', // 110 'Syrc', // 109 'Tibt', // 106 'Hanb', // 103 'Sund', // 102 'Laoo', // 100 'Mlym', // 99 'Taml', // 98 'Thai', // 97 'Gujr', // 94 'Khmr', // 94 'Brai', // 92 'Deva', // 89 'Cher', // 86 'Knda', // 86 'Mtei', // 86 'Telu', // 85 'Egyp', // 84 'Hani', // 84 'Ital', // 83 'Zinh', // 83 'Xpeo', // 82 'Xsux', // 82 'Hung', // 81 'Olck', // 81 // 61 to 80 'Adlm','Aran','Armi','Avst','Bali','Bopo','Bugi','Cakm', 'Cari','Copt','Cprt','Cyrs','Egyd','Egyh','Glag','Goth', 'Hang','Hira','Jamo','Java','Kana','Latf','Latg','Lina', 'Linb','Lyci','Lydi','Mand','Mani','Maya','Mero','Nkoo', 'Osma','Perm','Phli','Phlp','Phnx','Plrd','Prti','Rohg', 'Runr','Samr','Sgnw','Shaw','Syre','Syrj','Syrn','Talu', 'Tfng','Thaa','Ugar','Vaii','Visp','Yiii','Zsye', // under 60 'Afak','Aghb','Ahom','Bamu','Bass','Batk','Bhks','Blis', 'Brah','Buhd','Cham','Chrs','Cirt','Cpmn','Diak','Dogr', 'Dsrt','Dupl','Elba','Elym','Geok','Gong','Gonm','Gran', 'Hano','Hatr','Hluw','Hmng','Hmnp','Inds','Jurc','Kali', 'Kawi','Khar','Khoj','Kits','Kpel','Kthi','Lana','Lepc', 'Limb','Lisu','Loma','Mahj','Maka','Marc','Medf','Mend', 'Merc','Modi','Moon','Mroo','Mult','Nagm','Nand','Narb', 'Nbat','Newa','Nkgb','Nshu','Ogam','Orkh','Osge','Ougr', 'Palm','Pauc','Phag','Phlv','Rjng','Roro','Sara','Sarb', 'Saur','Shrd','Sidd','Sind','Sogd','Sogo','Sora','Soyo', 'Sylo','Tagb','Takr','Tale','Tang','Tavt','Teng','Tglg', 'Tirh','Tnsa','Toto','Vith','Wara','Wcho','Wole','Yezi', 'Zanb', // FYI: these seven are the same for all locales in blink 'Gara','Sunu','Tutg', // 4 'Gukh','Krai','Onao','Todr', // 3 //*/ ] aItems.sort() var list = gLocales, aLegend = [], aLocales = [], oTestData = {}, isSupported = false, localesHashAll = "" // to compare min to function testitems() { //loop each script on it's own dom.perf = "" dom.results = "" oTestData = {} try { aItems.forEach(function(item){ /* take one of the highest oTestData results and keep adding until you reach max item = [ 'Zxxx','Latn','Hans','Arab','Deva','Zzzz','Cyrl','Mymr','Mong','Beng','Orya','Guru', item ] //*/ // allow testing arrays of scripts let testarray = 'object' == typeof item ? item : [item] run_main('all', testarray, true) }) } catch(e) { console.log(e) } } function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DisplayNames.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method, aTest, isLoop = false) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = '<br><br>' let tests = {} let aUsed = undefined == aTest ? aItems: aTest if (!isLoop) { dom.perf = "" dom.results = "" } if (method == "all") { aUsed.sort() tests = {'script': {'long': aUsed, 'narrow': aUsed, 'short': aUsed}} } else { // long + narrow add nothing tests = {'script': {'short': ['Zxxx','Latn','Hans','Arab','Deva','Zzzz','Cyrl','Mymr','Mong','Beng','Orya','Guru','Hrkt']}} } try { aLocales.forEach(function(code) { // for each locale let oType = {} Object.keys(tests).sort().forEach(function(t){ // for each type oType[t] = {} for (const s of Object.keys(tests[t])) { // for each style oType[t][s] = ('all' == method) ? [] : {} let dn = new Intl.DisplayNames([code], {type: t, style: s}) tests[t][s].sort().forEach(function(item){ //item = item.toLowerCase() // blink is case sensitive let value = dn.of(item) if ('all' == method) { oType[t][s].push(item.toLowerCase() +': '+ value) } else { oType[t][s][item.toLowerCase()] = value } }) } }) let hash = mini(oType) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oType).forEach(function(typekey){ oTempData[hash][typekey] = oType[typekey] }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const key of Object.keys(oTempData[h]).sort()) { // for each granularity if (key == "locales") { oData[h][key] = oTempData[h][key].join(", ") } else { if (Object.keys(oTempData[h][key]).length) { oData[h][key] = oTempData[h][key] } } } } //console.log(oTempData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { // for each hash localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length if (!isLoop) { let str = "" for (const type of Object.keys(oData[k])) { // for each type if (type !== "locales") { str += "<li>"+ s14 + type + sc +"</li>" Object.keys(oData[k][type]).forEach(function(s){ // for each style if ('all' == method) { // color up the parts let aParts = [], array = oData[k][type][s] array.forEach(function(item){ // for each item let parts = item.split(":") item = item.replace(parts[0] +':', s12 + parts[0]+ ':' + sc) aParts.push(item) }) str += s16 + s.slice(0,1).toUpperCase() +": "+ sc + aParts.join(', ') +"</br>" } else { str += s16 + s + sc + '<ul class="main">' Object.keys(oData[k][type][s]).forEach(function(item){ // for each item str += "<li>"+ s12 + item +': '+ sc + oData[k][type][s][item] +"</li>" }) str += '</ul>' } }) } } if ('all' == method) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } } //console.log(oData) if (isLoop) { let testCount = localeGroups.length // record it if (undefined == oTestData[testCount]) {oTestData[testCount] = []} oTestData[testCount].push(aUsed) //console.log(testCount, aUsed) /* dom.results.innerHTML = dom.results.innerHTML + '<br>' + s16 + testCount + sc +': '+ '[\''+ aUsed.join('\', \'') +'\']' //*/ dom.results.innerHTML = dom.results.innerHTML + '<br>' + '\''+ aUsed.join('\', \'') +'\', // ' + testCount return } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "08964df0") { // FF151+ } else if (resultsHash == "4d2a0147") { // FF147-150 } else if (resultsHash == "4f0204b0") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "eeac2f94") { // FF147+ 195 } else if (localesHash == "552c1baf") { // FF140-146 193 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { let test = new Intl.DisplayNames(undefined, {type: 'region'}).resolvedOptions().locale isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message +'banana' } // add additional locales to core locales for this test let aListExtra = [ 'az-cyrl,azerbaijani (cyrillic)', 'bs-cyrl,bosnian (cyrillic)', 'en-au,english (australia)', 'en-gb,english (united kingdom)', 'en-in,english (india)', 'es-us,spanish (united states)', 'es-ar,spanish (argentina)', 'fa-af,persian (afghanistan)', 'ff-adlm,fulah (adlam)', 'fr-ca,french (canada)', 'hi-latn,hindi (latin)', 'kk-cn,kazakh (china)', 'kok-latn,konkani (latin)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'pa-arab,punjabi (arabic)', 'pt-ao,portuguese (angola)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'sr-latn,serbian (latin)', 'sw-ke,swahili (kenya)', 'uz-af,uzbek (afghanistan)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'yo-bj,yoruba (benin)', 'yue-cn,cantonese (china)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en','de','fr','pl','pt','sv'] //list = ['en'] legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/domrectspoof.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>domrect spoof detection</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} #tzpRect { background-color: #545aa7; top: 0; left: 0; width:100px; height:100px; transform: rotate(45deg); padding: 0px; opacity: 0.5; z-index: -20; position: fixed; /* must be fixed */ } </style> </head> <body> <div id="tzpRect"></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#elements">return to TZP index</a></td></tr> </table> <table id="tb15"> <col width="13%"><col width="12%"><col width="25%"><col width="50%"> <thead><tr><th colspan="4"> <div class="nav-title">domrect spoof detection <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="4" class="intro"> <span class="no_color">Measures a fixed element, rotated to forced decimal precision. On gecko this is an expected known result at any zoom level (try it: it auto-runs on resize) and on any platform. </span> </td></tr> <tr><td colspan="4"><hr></td></tr> <tr><td colspan="2" class="padr">expected gecko</td><td colspan="2" class="mono spaces" id="expected"></td></tr> <tr><td colspan="4"></td></tr> <tr><td colspan="4"><hr></td></tr> <tr><td colspan="3" class="padr">Element.getBoundingClientRect</td><td class="mono" id="res0"></td></tr> <tr><td colspan="3" class="padr">Element.getClientRects</td><td class="mono" id="res1"></td></tr> <tr><td colspan="3" class="padr">range.getBoundingClientRect</td><td class="mono" id="res2"></td></tr> <tr><td colspan="3" class="padr">range.getClientRects</td><td class="mono" id="res3"></td></tr> <tr><td colspan="4"></td></tr> <tr><td colspan="4"><hr></td></tr> <tr><td>history</td><td colspan="3" class="mono spaces" id="history"></td></tr> </table> <br> <script> 'use strict'; let aHistory = [] let target = dom.tzpRect let valid = "4447a487", valid1 = "f39a5bc7", valid2 = "dd03a1b3" let expected = [ s12 +" one: "+ sc + valid1, " x, left, y, top: -20.716659545898438", s12 +" two: "+ sc + valid2, " width, height: 141.41665649414062", " right, bottom: 120.69999694824219", s12 +"full: "+ sc + valid, ] let count = 0 dom.expected.innerHTML = expected.join("<br>") let sNames = ["Element.getBoundingClientRect", "Element.getClientRects", "Range.getBoundingClientRect", "Range.getClientRects"] let oBlinkNew = {} function get_rect() { if (aHistory.length > 19) {aHistory = aHistory.slice(-19)} // clear details sNames.forEach(function(name) {sDetail[name] = []}) let el = dom.tzpRect let res = [] /* boxquads: rules target.getBoxQuads() +'' == "[object DOMQuad]" target.getBoxQuads()[0] +'' == "[object DOMQuad]" target.getBoxQuads()[0].p1 +'' == "[object DOMPoint]" it's a fixed square ------------------- stable all w = 1, all z = 0 one of x or y == 50 ^ can get decimals e.g. at 133% zoom 50.000003814697266 ^ p1, p3 it is x ^ p2, p4 it is y the other of x or y ^ p1, p4 = same ^ p2, p3 = same stable hash = 6ec100c2 (with [object DOM*] checks) math the other we do math with: ^ p1, p3 it is y ^ p2, p4 it is x */ /* try { // with DPR of 1 floaties happen at 67% and 133% zoom only let box = el.getBoxQuads()[0] let p1 = box.p1, p2 = box.p2, p3 = box.p3, p4 = box.p4 let oStable = { 'a': el.getBoxQuads() +'', 'b': el.getBoxQuads()[0] +'', 'c': el.getBoxQuads()[0].p1 +'', 'match1y+4x': [p1.y == p4.x], 'match2x+3y': [p2.x == p3.y], 'p1': [p1.w, (Math.round((p1.x)*10000))/10000, p1.z], 'p2': [p2.w, (Math.round((p2.y)*10000))/10000, p2.z], 'p3': [p3.w, (Math.round((p3.x)*10000))/10000, p3.z], 'p4': [p4.w, (Math.round((p4.y)*10000))/10000, p4.z], } let stablehash = mini(oStable) // f73069e0 if ('6ec100c2' !== stablehash) { console.log(stablehash, oStable) } let aMath = [p2.x - p1.y, p3.y - p4.x] // one is width the other height //console.log(aMath) } catch(e) { //console.log(e+'') } //*/ for (let i=0; i < 4; i++) { let output = document.getElementById("res"+i) try { let obj = "" if (i == 0) { obj = el.getBoundingClientRect() } else if (i == 1) { obj = el.getClientRects()[0] } else { let range = document.createRange() range.selectNode(el) if (i == 2) { obj = range.getBoundingClientRect() } else { obj = range.getClientRects()[0] } } // 3 unique values, but cover all names let array1 = [obj.x, obj.left, obj.y, obj.top] let array2 = [obj.width, obj.height, obj.right, obj.bottom] let array = array1.concat(array2) let hash = mini(array.join()) let hash1 = mini(array1.join()) let hash2 = mini(array2.join()) if (isEngine == "blink") { if (!knownBlink.includes(hash)) { if (oBlinkNew[hash] == undefined) {oBlinkNew[hash] = array} } } // part 1 let display = hash1 if (isFF) {display += (hash1 == valid1 ? green_tick : red_cross)} // part 2 display += " | " + hash2 if (isFF) {display += (hash2 == valid2 ? green_tick : red_cross)} // always display details clickable unless known full gecko if (hash !== valid) { sDetail[sNames[i]] = array display += buildButton("15", sNames[i] +", true", "details") } output.innerHTML = display if (isFF) { res.push(hash + (mini(array.join()) == valid ? green_tick : red_cross)) } else { res.push(hash) } } catch(e) { output.innerHTML = e.name res.push(zErr) } } // add to history count++ if (res.length) { aHistory.push((count.toString()).padStart(3)+ ": " + res.join(" | ")) } dom.history.innerHTML = aHistory.join("<br>") } function outputBlink() { if (isEngine !== "blink") { return } if (Object.keys(oBlinkNew).length) { for (const hash of Object.keys(oBlinkNew)) { console.log(hash, oBlinkNew[hash]) } } } let knownBlink = [] let oBlinkSamples = { // windows dpr=1, dpi=1 // all zoom levels, default positions "2cd8d217": [-20.71068000793457, -20.71068000793457, -20.71068000793457, -20.71068000793457, 141.42135620117188, 141.42135620117188, 120.7106761932373, 120.7106761932373], "3fcff5df": [-20.710678100585938, -20.710678100585938, -20.710678100585938, -20.710678100585938, 141.42137145996094, 141.42137145996094, 120.710693359375, 120.710693359375], "5f81a4df": [-20.710678100585938, -20.710678100585938, -20.710678100585938, -20.710678100585938, 141.42135620117188, 141.42135620117188, 120.71067810058594, 120.71067810058594], "29e0447b": [-20.707441329956055, -20.707441329956055, -20.707441329956055, -20.707441329956055, 141.39926147460938, 141.39926147460938, 120.69182014465332, 120.69182014465332], "820458b3": [-20.71068000793457, -20.71068000793457, -20.71068000793457, -20.71068000793457, 141.42137145996094, 141.42137145996094, 120.71069145202637, 120.71069145202637], // zoom 500 "10d1949f": [-20.710681915283203, -20.710681915283203, -20.710681915283203, -20.710681915283203, 141.42137145996094, 141.42137145996094, 120.71068954467773, 120.71068954467773], "c3ec468d": [-20.710668563842773, -20.710668563842773, -20.710668563842773, -20.710668563842773, 141.42135620117188, 141.42135620117188, 120.7106876373291, 120.7106876373291], "2d2f0e42": [-20.710668563842773, -20.710668563842773, -20.710668563842773, -20.710668563842773, 141.42137145996094, 141.42135620117188, 120.71068954467773, 120.7106876373291], // 400 + 200 "1de05d90": [-20.710693359375, -20.710693359375, -20.710678100585938, -20.710678100585938, 141.42137145996094, 141.42135620117188, 120.71067810058594, 120.71067810058594], // 300 "db71bd4a": [-20.710674285888672, -20.710674285888672, -20.710683822631836, -20.710683822631836, 141.42135620117188, 141.42135620117188, 120.7106819152832, 120.71067237854004], "baf82439": [-20.710674285888672, -20.710674285888672, -20.710678100585938, -20.710678100585938, 141.42135620117188, 141.42137145996094, 120.7106819152832, 120.710693359375], "efce3af9": [-20.710674285888672, -20.710674285888672, -20.710678100585938, -20.710678100585938, 141.42135620117188, 141.42135620117188, 120.7106819152832, 120.71067810058594], "6a972f1e": [-20.710678100585938, -20.710678100585938, -20.710683822631836, -20.710683822631836, 141.42137145996094, 141.42135620117188, 120.710693359375, 120.71067237854004], "243cc010": [-20.710678100585938, -20.710678100585938, -20.710683822631836, -20.710683822631836, 141.42135620117188, 141.42135620117188, 120.71067810058594, 120.71067237854004], // 250 + 125 "61b13db7": [-20.710681915283203, -20.710681915283203, -20.710678100585938, -20.710678100585938, 141.42137145996094, 141.42135620117188, 120.71068954467773, 120.71067810058594], "a699afe9": [-20.710681915283203, -20.710681915283203, -20.710678100585938, -20.710678100585938, 141.42137145996094, 141.42137145996094, 120.71068954467773, 120.710693359375], // 175 "83d6e13b": [-20.710676193237305, -20.710676193237305, -20.710678100585938, -20.710678100585938, 141.42135620117188, 141.42135620117188, 120.71068000793457, 120.71067810058594], "2ec16097": [-20.710676193237305, -20.710676193237305, -20.710676193237305, -20.710676193237305, 141.42135620117188, 141.42135620117188, 120.71068000793457, 120.71068000793457], // 150 "3710b115": [-20.710678100585938, -20.710678100585938, -20.710681915283203, -20.710681915283203, 141.42135620117188, 141.42135620117188, 120.71067810058594, 120.71067428588867], // 125 "9b7056e0": [-20.710683822631836, -20.710683822631836, -20.710678100585938, -20.710678100585938, 141.42135620117188, 141.42137145996094, 120.71067237854004, 120.710693359375], "7b7a02ee": [-20.710683822631836, -20.710683822631836, -20.710678100585938, -20.710678100585938, 141.42135620117188, 141.42135620117188, 120.71067237854004, 120.71067810058594], // 110 "41da0d9e": [-20.710683822631836, -20.710683822631836, -20.710670471191406, -20.710670471191406, 141.42135620117188, 141.4213409423828, 120.71067237854004, 120.7106704711914], "9395cf12": [-20.710678100585938, -20.710678100585938, -20.710670471191406, -20.710670471191406, 141.4213409423828, 141.4213409423828, 120.71066284179688, 120.7106704711914], "588e14ad": [-20.710678100585938, -20.710678100585938, -20.710670471191406, -20.710670471191406, 141.42135620117188, 141.4213409423828, 120.71067810058594, 120.7106704711914], "2033442d": [-20.710678100585938, -20.710678100585938, -20.710670471191406, -20.710670471191406, 141.42137145996094, 141.4213409423828, 120.710693359375, 120.7106704711914], "ab629b83": [-20.710678100585938, -20.710678100585938, -20.710670471191406, -20.710670471191406, 141.42137145996094, 141.42135620117188, 120.710693359375, 120.71068572998047], "bbe63d75": [-20.710678100585938, -20.710678100585938, -20.710670471191406, -20.710670471191406, 141.42135620117188, 141.42135620117188, 120.71067810058594, 120.71068572998047], // 100 "d468b282": [-20.710678100585938, -20.710678100585938, -20.710693359375, -20.710693359375, 141.42135620117188, 141.42137145996094, 120.71067810058594, 120.71067810058594], // 90 "1067b86e": [-20.71068000793457, -20.71068000793457, -20.7106876373291, -20.7106876373291, 141.42135620117188, 141.42137145996094, 120.7106761932373, 120.71068382263184], "af2cc7d9": [-20.71068000793457, -20.71068000793457, -20.7106876373291, -20.7106876373291, 141.42137145996094, 141.42137145996094, 120.71069145202637, 120.71068382263184], // 75 "6403ea2f": [-20.710678100585938, -20.710678100585938, -20.710674285888672, -20.710674285888672, 141.42137145996094, 141.42135620117188, 120.710693359375, 120.7106819152832], "d9c4977d": [-20.710678100585938, -20.710678100585938, -20.710674285888672, -20.710674285888672, 141.42135620117188, 141.42135620117188, 120.71067810058594, 120.7106819152832], "0a1577ed": [-20.710678100585938, -20.710678100585938, -20.710678100585938, -20.710678100585938, 141.42137145996094, 141.42135620117188, 120.710693359375, 120.71067810058594], // 67 "518c2bea": [-20.707441329956055, -20.707441329956055, -20.707443237304688, -20.707443237304688, 141.39926147460938, 141.39926147460938, 120.69182014465332, 120.69181823730469], // other? "11882a7d": [-20.710678100585938, -20.710678100585938, -20.710693359375, -20.710693359375, 141.42135620117188, 141.42138671875, 120.71067810058594, 120.710693359375], } for (const hash of Object.keys(oBlinkSamples)) { knownBlink.push(hash) } function testBlink() { for (const hash of Object.keys(oBlinkSamples)) { let array = oBlinkSamples[hash] if (mini(array.join()) !== hash) {console.log(hash, "hash mismatch")} let x = array[0], left = array[1] let y = array[2], top = array[3] let width = array[4], height = array[5] let right = array[6], bottom = array[7] let aLies = [] if (x !== left) { aLies.push(["x !== left", x, left].join(", ")) } if (y !== top) { aLies.push(["y !== top", y, top].join(", ")) } if (right - x !== width) { aLies.push(["right - x !== width", right, x, "expected "+ width, "got "+ (right - x), "diff "+ ((right - x) - width)].join(", ")) } if (bottom - y !== height) { aLies.push(["bottom - y !== height", bottom, y, "expected "+ height, "got "+ (bottom - y), "diff "+ ((bottom - y) - height)].join(", ")) } if (aLies.length) { console.log(hash +"\n - "+ aLies.join("\n - ")) } } } Promise.all([ get_globals() ]).then(function(){ window.addEventListener("resize", get_rect) get_rect() }) </script> </body> </html> ================================================ FILE: tests/domrectspoofratio.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>domrect spoof aspect ratio</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 780px;} #tzpRect { background-color: #545aa7; top: 0; left: 0; width:100px; height:100px; /* scaleX, scaleY, skewX, skewY, translateX, translateY */ transform: matrix(.3333, 1.6666, 1, 1, 100, 100); padding: 0px; opacity: 0.5; z-index: -20; position: fixed; } </style> </head> <body> <div id="tzpRect"></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#elements">return to TZP index</a></td></tr> </table> <table id="tb15"> <col width="1%"><col width="29%"><col width="70%"> <thead><tr><th colspan="3"> <div class="nav-title">domrect spoof detection: aspect ratio <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="3" class="intro"> <span class="no_color">Measures a fixed element, transformed to forced decimal precision with a known aspect ratio to check if width or height have been tampered with. </span> </td></tr> <tr><td colspan="3"><hr></td></tr> <tr><td colspan="2" class="padr">Element.getBoundingClientRect</td><td class="mono" id="res0"></td></tr> <tr><td colspan="2" class="padr">Element.getClientRects</td><td class="mono" id="res1"></td></tr> <tr><td colspan="2" class="padr">range.getBoundingClientRect</td><td class="mono" id="res2"></td></tr> <tr><td colspan="2" class="padr">range.getClientRects</td><td class="mono" id="res3"></td></tr> <tr><td colspan="3"></td></tr> <tr><td colspan="3"><hr></td></tr> <tr><td>history</td><td colspan="2" class="mono spaces" id="history"></td></tr> </table> <br> <script> 'use strict'; let aHistory = [] let count = 0 let sNames = ["Element.getBoundingClientRect", "Element.getClientRects", "Range.getBoundingClientRect", "Range.getClientRects"] let knownBlink = [ // standard 0.5, 0.4999999427781113, 0.5000000572218888, // when page is scrolled so no longer top left = 0 0.4999999427781178, 0.4999998855562356, ] function get_rect() { if (aHistory.length > 19) {aHistory = aHistory.slice(-19)} let el = dom.tzpRect let res = [] for (let i=0; i < 4; i++) { let output = document.getElementById("res"+i) try { let obj = "" if (i == 0) { obj = el.getBoundingClientRect() } else if (i == 1) { obj = el.getClientRects()[0] } else { let range = document.createRange() range.selectNode(el) if (i == 2) { obj = range.getBoundingClientRect() } else { obj = range.getClientRects()[0] } } let array = [obj.width, obj.height, (obj.width / obj.height)] let display = array.join(" | ") let mark = "" if (isFF) { mark = array[2] == 0.5000000572204611 ? green_tick : red_cross } else if (isEngine == "blink") { mark = knownBlink.includes(array[2]) ? green_tick : red_cross } output.innerHTML = display + mark res.push(array[2] + mark) } catch(e) { output.innerHTML = e.name res.push(zErr) } } // add to history count++ if (res.length) { aHistory.push((count.toString()).padStart(3)+ ": " + res.join(" | ")) } dom.history.innerHTML = aHistory.join("<br>") } Promise.all([ get_globals() ]).then(function(){ window.addEventListener("resize", get_rect) get_rect() }) </script> </body> </html> ================================================ FILE: tests/dtfcomponents.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>dtf: components</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2"> <div class="nav-title">datetimeformat: components <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy with DateTimeFormat date-time components</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = "", // to compare min to oData = {} function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DateTimeFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method = "all") { let t0 = performance.now() let spacer = "<br><br>" // all dates (days/months/am-pm) must be timezone resistent: we are checking locale only // timezonename (and locale) is tested in the a different PoC - see TZP set_oIntlDateTests section // thus we use UTC time so everyone uses the exact same dates, and then we pass // UTC as the timezone so nothing shifts, preserving our specific datetimes // note: only some sub-tests actually expose the day and/or hr let dates = { // fractionalSecondDigits: we only ever reveal the seconds FSD: new Date('2023-06-10T01:12:34.567Z'), // month (x4) + year (xJan): we only ever reveal the month or year Jan: new Date('2023-01-15T00:00:00.000Z'), Feb: new Date('2023-02-15T00:00:00.000Z'), Mar: new Date('2023-03-15T00:00:00.000Z'), Apr: new Date('2023-04-15T00:00:00.000Z'), May: new Date('2023-05-15T00:00:00.000Z'), Jun: new Date('2023-06-15T00:00:00.000Z'), Jul: new Date('2023-07-15T00:00:00.000Z'), Aug: new Date('2023-08-15T00:00:00.000Z'), Sep: new Date('2023-09-15T00:00:00.000Z'), Oct: new Date('2023-10-15T00:00:00.000Z'), Nov: new Date('2023-11-15T00:00:00.000Z'), Dec: new Date('2023-12-15T00:00:00.000Z'), // days (x2) + hrs (xFri) expose doh! days + hrs Day: new Date("2003-01-01T01:00:00.000Z"), // single digit day Sat: new Date('2023-01-14T01:00:00.000Z'), Sun: new Date('2023-01-15T01:00:00.000Z'), Mon: new Date('2023-01-16T01:00:00.000Z'), Tue: new Date('2023-01-17T01:00:00.000Z'), Wed: new Date('2023-01-18T01:00:00.000Z'), // doubles as hour 1 + minute 0 Thu: new Date('2023-01-19T01:00:00.000Z'), Fri: new Date('2023-01-20T13:00:00.000Z'), // doubles as hour 13 // era: expose day Era1: new Date('-000002-01-15T01:00:00.000Z'), // BC Era2: new Date('2001-01-01T01:00:00.000Z'), // AD } // https://www.w3schools.com/jsref/jsref_tolocalestring.asp let testsAll = { "day": { dates: [dates.Day], options: { "2-digit": {day: "2-digit"}, "numeric": {day: "numeric"}, }, }, "era": { dates: [dates.Era1, dates.Era2], options: { "long": {era: "long"}, "narrow": {era: "narrow"}, "short": {era: "short"}, }, }, // this adds nothing on it's own: what adds entropy is the delimiters and decimal separator "fractionalSecondDigits": { dates: [dates.FSD], options: { "1": {minute: "numeric", second: 'numeric', fractionalSecondDigits: 1}, "2": {minute: "numeric", second: 'numeric', fractionalSecondDigits: 2}, "3": {minute: "numeric", second: 'numeric', fractionalSecondDigits: 3}, }, }, "hour": { dates: [dates.Wed, dates.Fri], options: { "2-digit": {hour: "2-digit"}, "numeric": {hour: "numeric"}, }, }, "hourCycle": { dates: [dates.Wed], options: { "h112": {hour: "2-digit", hourCycle: "h11"}, // these add nothing except perf cost! //"h11-n": {hour: "numeric", hourCycle: "h11"}, //"h12-2": {hour: "2-digit", hourCycle: "h12"}, //"h12-n": {hour: "numeric", hourCycle: "h12"}, //"h23-2": {hour: "2-digit", hourCycle: "h23"}, //"h23-n": {hour: "numeric", hourCycle: "h23"}, //"h24-2": {hour: "2-digit", hourCycle: "h24"}, //"h24-n": {hour: "numeric", hourCycle: "h24"}, }, }, "minute": { dates: [dates.Wed], // single digit 0 vs 00 options: { "2-digit": {minute: "2-digit"}, "numeric": {minute: "numeric"}, }, }, "month": { dates: [ dates.Jan, dates.Feb, dates.Mar, dates.Apr, dates.May, dates.Jun, dates.Jul, dates.Aug, dates.Sep, dates.Oct, dates.Nov, dates.Dec, ], options: { "2-digit": {month: "2-digit"}, "long": {month: "long"}, "narrow": {month: "narrow"}, "numeric": {month: "numeric"}, "short": {month: "short"}, }, }, "second": { dates: [dates.FSD], options: { "2-digit": {second: "2-digit"}, "numeric": {second: "numeric"}, }, }, "weekday": { dates: [dates.Mon, dates.Tue, dates.Wed, dates.Thu, dates.Fri, dates.Sat, dates.Sun], options: { "long": {weekday: "long"}, "narrow": {weekday: "narrow"}, "short": {weekday: "short"}, }, }, "year": { dates: [dates.Jan], options: { "2-digit": {year: "2-digit"}, "numeric": {year: "numeric"}, }, }, } let testsMin = { "era": [ // to match TZP we will add controlling the year-month-day part (in TZP this is needed to get toLocaleString to match) [dates.Era1, {"long": {era: "long", year: "numeric", month: "numeric", day: "numeric"}}], ], "fractionalSecondDigits": [ [dates.FSD, {"1": {minute: "numeric", second: 'numeric', fractionalSecondDigits: 1}}] ], "hour": [ [dates.Wed, {"numeric": {hour: "numeric"}}], ], "hourCycle": [ [dates.Wed, {"h112": {hour: "2-digit", hourCycle: "h11"}}], // adds like 2 items ], "month": [ [dates.Nov, {"narrow": {month: "narrow"}}], [dates.Jan, {"short": {month: "short"}}], [dates.Jun, {"short": {month: "short"}}], // needed for FF146 and lower [dates.Sep, {"short": {month: "short"}}], [dates.Nov, {"short": {month: "short"}}], ], "weekday": [ [dates.Wed, {"narrow": {weekday: "narrow"}, "long": {weekday: "long"}}], [dates.Fri, {"short": {weekday: "short"}, "narrow": {weekday: "narrow"}, "long": {weekday: "long"}}], ], "year": [ // needed for blink [dates.Jan, {"2-digit": {year: "2-digit"}}], ], } let tests = method == "all" ? testsAll : testsMin //console.log(tests) let oConst = {} oData = {} //aLocales = ['en'] try { aLocales.forEach(function(code) { let oTempData = {} Object.keys(tests).sort().forEach(function(test) { oTempData[test] = {} if (method == "all") { Object.keys(tests[test].options).forEach(function(opt) { oTempData[test][opt] = [] let options = tests[test].options[opt] options['timeZone'] = "UTC" let formatter = new Intl.DateTimeFormat(code, options) let dates = tests[test].dates dates.forEach(function(date){ oTempData[test][opt].push(formatter.format(date)) }) }) } else { try {oConst.WeS = new Intl.DateTimeFormat(code, {weekday: "short", timeZone: "UTC"})} catch(e) {} try {oConst.WeN = new Intl.DateTimeFormat(code, {weekday: "narrow", timeZone: "UTC"})} catch(e) {} try {oConst.WeL = new Intl.DateTimeFormat(code, {weekday: "long", timeZone: "UTC"})} catch(e) {} try {oConst.MoS = new Intl.DateTimeFormat(code, {month: "short", timeZone: "UTC"})} catch(e) {} try {oConst.MoN = new Intl.DateTimeFormat(code, {month: "narrow", timeZone: "UTC"})} catch(e) {} try {oConst.HoN = new Intl.DateTimeFormat(code, {hour: "numeric", timeZone: "UTC"})} catch(e) {} try {oConst.HoH = new Intl.DateTimeFormat(code, {hour: "2-digit", hourCycle: "h11", timeZone: "UTC"})} catch(e) {} try {oConst.ErL = new Intl.DateTimeFormat(code, {era: "long", timeZone: "UTC"})} catch(e) {} try {oConst.Fr1 = new Intl.DateTimeFormat(code, {minute: "numeric", second: 'numeric', timeZone: "UTC", fractionalSecondDigits: 1})} catch(e) {} try {oConst.Fr2 = new Intl.DateTimeFormat(code, {minute: "numeric", second: 'numeric', timeZone: "UTC", fractionalSecondDigits: 2})} catch(e) {} try {oConst.Fr3 = new Intl.DateTimeFormat(code, {minute: "numeric", second: 'numeric', timeZone: "UTC", fractionalSecondDigits: 3})} catch(e) {} try {oConst.Ye2 = new Intl.DateTimeFormat(code, {year: "2-digit", timeZone: "UTC"})} catch(e) {} /* only maybe used if trying to test other values outside of current min try {oConst.ErN = new Intl.DateTimeFormat(code, {era: "narrow", timeZone: "UTC"})} catch(e) {} try {oConst.ErS = new Intl.DateTimeFormat(code, {era: "short", timeZone: "UTC"})} catch(e) {} try {oConst.MoL = new Intl.DateTimeFormat(code, {month: "long", timeZone: "UTC"})} catch(e) {} try {oConst.Se2 = new Intl.DateTimeFormat(code, {second: "2-digit", timeZone: "UTC"})} catch(e) {} try {oConst.SeN = new Intl.DateTimeFormat(code, {second: "numeric", timeZone: "UTC"})} catch(e) {} try {oConst.YeN = new Intl.DateTimeFormat(code, {year: "numeric", timeZone: "UTC"})} catch(e) {} //*/ let array = tests[test] array.forEach(function(item) { let date = item[0] let styles = item[1] Object.keys(styles).forEach(function(opt) { if (oTempData[test][opt] == undefined) { oTempData[test][opt] = [] } let constructor = test.slice(0,1).toUpperCase() + test.slice(1,2) + opt.slice(0,1).toUpperCase() //console.log(test.slice(0,1).toUpperCase() + test.slice(1,2) + opt.slice(0,1).toUpperCase()) let formatter = oConst[constructor] oTempData[test][opt].push(formatter.format(date)) }) }) } }) let hash = mini(oTempData) //console.log(hash, oTempData) if (oData[hash] == undefined) { oData[hash] = {} oData[hash]["locales"] = [code] oData[hash]["data"] = {} for (const k of Object.keys(oTempData).sort()) { oData[hash]["data"][k] = oTempData[k] } } else { oData[hash]["locales"].push(code) } }) // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" //console.log(oData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].length let str = "" for (const p of Object.keys(oData[k]["data"])) { str += s14 + p +": "+ sc for (const i of Object.keys(oData[k]["data"][p]).sort()) { let option let slicelen = (p == "hourCycle" ? 5 : 1) if (i == "numeric") { option = "Num" } else { option = i.slice(0,slicelen).toUpperCase() } str += "<li>"+ s16 + option +": "+ sc + oData[k]["data"][p][i].join(", ") +"</li>" } } displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"].join(", ") +"</li></ul>" ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // ignore if non-supported used, which return same as undefined = user's resolved options // results if (resultsHash == "d3d21dbf") { // FF151+ } else if (resultsHash == "6e33774c") { // FF147-150 } else if (resultsHash == "e0558272") { // FF140-146+ } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "bfb86c10") { // FF151+: 326 } else if (localesHash == "b4751570") { // FF147+: 325 } else if (localesHash == "32f59b41") { // FF140-146: 326 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + localeGroups.length + sc +" from "+ s4 + aLocales.length + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { // pointless if we can't use the feature being tested: FF58+ let test = new Intl.DateTimeFormat("en").formatToParts(new Date) isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // add additional locales to core locales for this test let aListExtra = [ "af-na,afrikaans (namibia)", "ar-ae,arabic (united arabic emirates)", "ar-dz,arabic (algeria)", "ar-il,arabic (israel)", "ar-iq,arabic (iraq)", "ar-ly,arabic (libya)", "ar-ma,arabic (morocco)", "ar-mr,arabic (mauritania)", "ar-sa,arabic (saudi arabia)", "az-cyrl,azerbaijani (cyrillic)", "bn-in,bengali (india)", "bo-in,tibetan (india)", "bs-cyrl,bosnian (cyrillic)", "ckb-ir,central kurdish (iran)", "de-at,german (austria)", "de-ch,german (switzerland)", "de-lu,german (luxembourg)", "ee-tg,éwé (togo)", "en-ag,english (antigua & barbuda)", "en-ai,english (anguilla)", "en-at,english (austria)", "en-au,english (australia)", "en-bi,english (burundi)", "en-ca,english (canada)", "en-ch,english (switzerland)", "en-dk,english (denmark)", "en-gb,english (united kingdom)", "en-ie,english (ireland)", "en-il,english (israel)", "es-419,spanish (latin america and the caribbean)", "es-ar,spanish (argentina)", "es-bz,spanish (belize)", "es-cl,spanish (chile)", "es-co,spanish (colombia)", "es-do,spanish (dominican republic)", "es-mx,spanish (mexico)", "es-pe,spanish (peru)", "es-ph,spanish (philippines)", "es-uy,spanish (uruguay)", "es-ve,spanish (venezuela)", "fa-af,persian (afghanistan)", "ff-adlm,fulah (adlam)", "ff-adlm-gh,fulah (adlam ghana)", "ff-gh,fulah (ghana)", "fr-ca,french (canada)", "fr-cm,french (cameroon)", "fr-dj,french (djibouti)", "fr-ma,french (morocco)", "ha-gh,hausa (ghana)", "hi-latn,hindi (latin)", "hr-ba,croatian (bosnia & herzegovina)", "it-ch,italian (switzerland)", 'kk-cn,kazakh (china)', "kok-latn,konkani (latin)", "ks-deva,kashmiri (devanagari)", "kxv-telu,kuvi (telugu)", "lrc-iq,northern luri (iraq)", "ms-bn,malay (brunei)", "ms-id,malay (indonesia)", "ne-in,nepali (india)", "om-ke,oromo (kenya)", "pa-arab,punjabi (arabic)", "ps-pk,pashto (pakistan)", "pt-ao,portuguese (angola)", "pt-mo,portuguese (macau)", "qu-bo,quechua (bolivia)", "ro-md,romanian (moldova)", "sd-deva,sindhi (devanagari)", "se-fi,northern sami (finland)", "shi-latn,tachelhit (latin)", "so-ke,somali (kenya)", "sq-mk,albanian (macedonia)", "sr-ba,serbian (bosnia & herzegovina)", "sr-cyrl-me,serbian (cyrillic montenegro)", "sr-cyrl-xk,serbian (cyrillic kosovo)", "sr-latn,serbian (latin)", "sr-latn-ba,serbian (latin bosnia & herzegovina)", "sr-latn-me,serbian (latin montenegro)", "sr-latn-xk,serbian (latin kosovo)", 'st-ls,southern sotho', 'sv-ax,swedish (åland islands)', "sv-fi,swedish (finland)", "sw-cd,swahili (congo kinshasa)", "ta-lk,tamil (sri lanka)", "ti-er,tigrinya (eritrea)", "tr-cy,turkish (cyprus)", "ur-in,urdu (india)", "uz-af,uzbek (afghanistan)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "vai-latn,vai (latin)", "yo-bj,yoruba (benin)", "yue-hans,cantonese (simplified)", "zh-hans-hk,chinese (simplified hong kong)", // blink 'ar-bh,arabic (bahrain)', 'uz-arab,uzbek (arabic)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en,english'] legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dtfdatetimestyle.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>dtf: date-&-timestyle</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 680px; max-width: 780px;} ul.main {margin-left: -20px;} li.dates {margin-left: 10px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2"> <div class="nav-title">datetimeformat: date-&-timestyle <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy with DateTimeFormat date-&-timeStyles. Numbers will vary depending on the timezone. To check all timezones, type <code>run_all()</code> in the console and go make a cup of tea.</span> <span id="alert"></span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <select name="timezones" id="timezones" onChange="run(`all`)"><option></option></select> <span class="btn4 btnfirst" onClick="run('next')"> &nbsp; [ &#9654; ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = '', // to compare min to oData = {}, oDates = {}, tzData = {}, tzDataAll = {}, isMax, isMin, aTZ = [], oCheck = {}, isAuto = false, spacer = '<br><br>' function log_console(name) { let hash = mini(sDetail[name]) if (name == 'locales') { console.log(name +': ' + hash +'\n'+ sDetail['locales'].join('\n')) } else { console.log(name +': ' + hash +'\n', sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { // populate tz options once as well try { try { aTZ = Intl.supportedValuesOf('timeZone') } catch(e) {} // supportedValuesOf not supported // add anything else that will work let testDate = new Date() let testTZ = [] gTimezones.forEach(function(n) { try { let test = testDate.toLocaleString('en', {timeZone: n}) testTZ.push(n) } catch(e) {} }) // is there anything in supported not in gTimezones let aMissing = aTZ.filter(x => !gTimezones.includes(x)) if (aMissing.length) { console.log('suported timezones missing in gTimezones\n', aMissing) dom.alert.innerHTML = '<br>'+ s1 +'[alert] supported timezones missing in gTimezones'+ sc } aTZ = aTZ.concat(testTZ) aTZ = aTZ.filter(function(item, position) {return aTZ.indexOf(item) === position}) aTZ.sort() //aTZ =['America/Bogota','America/Detroit','America/Havana'] // chrome keeps crashing the tab with run_all : break into bits and test manually //aTZ = aTZ.slice(400, 600) // 146 - 150 // arrange into an object let optTZ = {} aTZ.forEach(function(n) { let group, name if (n.includes('/')) { group = n.split('/')[0] name = n.slice(group.length +1) //+1 remove the '/' } else { group = 'AAA' // tmp group name, we want it first alphabetically name = n } if (optTZ[group] == undefined) {optTZ[group] = []} optTZ[group].push(name) }) //console.log(optTZ) let aOptions = [] // always add undefined as our first if (isFF) {aOptions.push("<optgroup label = '"+ "misc" +"'>")} aOptions.push("<option value = '"+ "undefined" +"'> "+ "undefined" +"</option>") for (const k of Object.keys(optTZ).sort()) { let prefix = k+'/' if (k == 'AAA') { prefix = '' } else { aOptions.push('<hr>') if (isFF) { // blink doesn't fully populate with optgroup aOptions.push("<optgroup label = '"+ k.toUpperCase() +"'>") } } optTZ[k].forEach(function(tz) { aOptions.push("<option value = '"+ prefix + tz +"'> "+ prefix + tz +"</option>") }) } dom.timezones.innerHTML = aOptions.join('') } catch(e) { console.log(e) } list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(',')[0].trim() let name = (undefined !== str.split(',')[1]) ? str.split(',')[1].trim() : '' let go = true if (isSupported) { go = Intl.DateTimeFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes('(') && (name.length + code.length) > 32) if (name.includes('(')) { let name0 = name.split('(')[0].trim() let name1 = name.substring( name.indexOf('(') + 1, name.lastIndexOf(')') ) name1 = s99 +'('+ name1 + ')'+ sc if (isSplit) { name = name0 +'<br>'+ ' '.repeat(4) + name1 } else { name = name0 +' '+ name1 } } aLegend.push(code.padStart(7) +': '+ name) } } } // output let header = s4 +'LEGEND ['+ aLegend.length +']'+ sc +'<br><br>' dom.legend.innerHTML = header + aLegend.join('<br>') } function set_dates() { // if we use UTC then we can check the original date hasn't been altered // but we will have to end up testing more dates to cover specific days // timezones can be 14 hrs less or 12 hrs more but (IIUIC) our selected dates aren't hiting those instances // where it exceeds ±12 (or I lucked out) and we end up only needing two identical times on subsequent days // lets do 7 days per month and two times per day // this gives us a maximum result to start from oDates = { JanA: new Date('2024-01-02T04:12:34.000Z'), JanB: new Date('2024-01-02T14:12:34.000Z'), JanC: new Date('2024-01-03T04:12:34.000Z'), JanD: new Date('2024-01-03T14:12:34.000Z'), JanE: new Date('2024-01-04T04:12:34.000Z'), JanF: new Date('2024-01-04T14:12:34.000Z'), JanG: new Date('2024-01-05T04:12:34.000Z'), JanH: new Date('2024-01-05T14:12:34.000Z'), JanI: new Date('2024-01-06T04:12:34.000Z'), JanJ: new Date('2024-01-06T14:12:34.000Z'), JanK: new Date('2024-01-07T04:12:34.000Z'), JanL: new Date('2024-01-07T14:12:34.000Z'), JanM: new Date('2024-01-08T04:12:34.000Z'), JanN: new Date('2024-01-08T14:12:34.000Z'), MayA: new Date('2024-05-02T04:12:34.000Z'), MayB: new Date('2024-05-02T14:12:34.000Z'), MayC: new Date('2024-05-03T04:12:34.000Z'), MayD: new Date('2024-05-03T14:12:34.000Z'), MayE: new Date('2024-05-04T04:12:34.000Z'), MayF: new Date('2024-05-04T14:12:34.000Z'), MayG: new Date('2024-05-05T04:12:34.000Z'), MayH: new Date('2024-05-05T14:12:34.000Z'), MayI: new Date('2024-05-06T04:12:34.000Z'), MayJ: new Date('2024-05-06T14:12:34.000Z'), MayK: new Date('2024-05-07T04:12:34.000Z'), MayL: new Date('2024-05-07T14:12:34.000Z'), MayM: new Date('2024-05-08T04:12:34.000Z'), MayN: new Date('2024-05-08T14:12:34.000Z'), JulA: new Date('2024-07-02T04:12:34.000Z'), JulB: new Date('2024-07-02T14:12:34.000Z'), JulC: new Date('2024-07-03T04:12:34.000Z'), JulD: new Date('2024-07-03T14:12:34.000Z'), JulE: new Date('2024-07-04T04:12:34.000Z'), JulF: new Date('2024-07-04T14:12:34.000Z'), JulG: new Date('2024-07-05T04:12:34.000Z'), JulH: new Date('2024-07-05T14:12:34.000Z'), JulI: new Date('2024-07-06T04:12:34.000Z'), JulJ: new Date('2024-07-06T14:12:34.000Z'), JulK: new Date('2024-07-07T04:12:34.000Z'), JulL: new Date('2024-07-07T14:12:34.000Z'), JulM: new Date('2024-07-08T04:12:34.000Z'), JulN: new Date('2024-07-08T14:12:34.000Z'), SepA: new Date('2024-09-02T04:12:34.000Z'), SepB: new Date('2024-09-02T14:12:34.000Z'), SepC: new Date('2024-09-03T04:12:34.000Z'), SepD: new Date('2024-09-03T14:12:34.000Z'), SepE: new Date('2024-09-04T04:12:34.000Z'), SepF: new Date('2024-09-04T14:12:34.000Z'), SepG: new Date('2024-09-05T04:12:34.000Z'), SepH: new Date('2024-09-05T14:12:34.000Z'), SepI: new Date('2024-09-06T04:12:34.000Z'), SepJ: new Date('2024-09-06T14:12:34.000Z'), SepK: new Date('2024-09-07T04:12:34.000Z'), SepL: new Date('2024-09-07T14:12:34.000Z'), SepM: new Date('2024-09-08T04:12:34.000Z'), SepN: new Date('2024-09-08T14:12:34.000Z'), NovA: new Date('2024-11-02T04:12:34.000Z'), NovB: new Date('2024-11-02T14:12:34.000Z'), NovC: new Date('2024-11-03T04:12:34.000Z'), NovD: new Date('2024-11-03T14:12:34.000Z'), NovE: new Date('2024-11-04T04:12:34.000Z'), NovF: new Date('2024-11-04T14:12:34.000Z'), NovG: new Date('2024-11-05T04:12:34.000Z'), NovH: new Date('2024-11-05T14:12:34.000Z'), NovI: new Date('2024-11-06T04:12:34.000Z'), NovJ: new Date('2024-11-06T14:12:34.000Z'), NovK: new Date('2024-11-07T04:12:34.000Z'), NovL: new Date('2024-11-07T14:12:34.000Z'), NovM: new Date('2024-11-08T04:12:34.000Z'), NovN: new Date('2024-11-08T14:12:34.000Z'), FebA: new Date('2024-02-02T04:12:34.000Z'), FebB: new Date('2024-02-02T14:12:34.000Z'), FebC: new Date('2024-02-03T04:12:34.000Z'), FebD: new Date('2024-02-03T14:12:34.000Z'), FebE: new Date('2024-02-04T04:12:34.000Z'), FebF: new Date('2024-02-04T14:12:34.000Z'), FebG: new Date('2024-02-05T04:12:34.000Z'), FebH: new Date('2024-02-05T14:12:34.000Z'), FebI: new Date('2024-02-06T04:12:34.000Z'), FebJ: new Date('2024-02-06T14:12:34.000Z'), FebK: new Date('2024-02-07T04:12:34.000Z'), FebL: new Date('2024-02-07T14:12:34.000Z'), FebM: new Date('2024-02-08T04:12:34.000Z'), FebN: new Date('2024-02-08T14:12:34.000Z'), MarA: new Date('2024-03-02T04:12:34.000Z'), MarB: new Date('2024-03-02T14:12:34.000Z'), MarC: new Date('2024-03-03T04:12:34.000Z'), MarD: new Date('2024-03-03T14:12:34.000Z'), MarE: new Date('2024-03-04T04:12:34.000Z'), MarF: new Date('2024-03-04T14:12:34.000Z'), MarG: new Date('2024-03-05T04:12:34.000Z'), MarH: new Date('2024-03-05T14:12:34.000Z'), MarI: new Date('2024-03-06T04:12:34.000Z'), MarJ: new Date('2024-03-06T14:12:34.000Z'), MarK: new Date('2024-03-07T04:12:34.000Z'), MarL: new Date('2024-03-07T14:12:34.000Z'), MarM: new Date('2024-03-08T04:12:34.000Z'), MarN: new Date('2024-03-08T14:12:34.000Z'), AprA: new Date('2024-04-02T04:12:34.000Z'), AprB: new Date('2024-04-02T14:12:34.000Z'), AprC: new Date('2024-04-03T04:12:34.000Z'), AprD: new Date('2024-04-03T14:12:34.000Z'), AprE: new Date('2024-04-04T04:12:34.000Z'), AprF: new Date('2024-04-04T14:12:34.000Z'), AprG: new Date('2024-04-05T04:12:34.000Z'), AprH: new Date('2024-04-05T14:12:34.000Z'), AprI: new Date('2024-04-06T04:12:34.000Z'), AprJ: new Date('2024-04-06T14:12:34.000Z'), AprK: new Date('2024-04-07T04:12:34.000Z'), AprL: new Date('2024-04-07T14:12:34.000Z'), AprM: new Date('2024-04-08T04:12:34.000Z'), AprN: new Date('2024-04-08T14:12:34.000Z'), JunA: new Date('2024-06-02T04:12:34.000Z'), JunB: new Date('2024-06-02T14:12:34.000Z'), JunC: new Date('2024-06-03T04:12:34.000Z'), JunD: new Date('2024-06-03T14:12:34.000Z'), JunE: new Date('2024-06-04T04:12:34.000Z'), JunF: new Date('2024-06-04T14:12:34.000Z'), JunG: new Date('2024-06-05T04:12:34.000Z'), JunH: new Date('2024-06-05T14:12:34.000Z'), JunI: new Date('2024-06-06T04:12:34.000Z'), JunJ: new Date('2024-06-06T14:12:34.000Z'), JunK: new Date('2024-06-07T04:12:34.000Z'), JunL: new Date('2024-06-07T14:12:34.000Z'), JunM: new Date('2024-06-08T04:12:34.000Z'), JunN: new Date('2024-06-08T14:12:34.000Z'), AugA: new Date('2024-08-02T04:12:34.000Z'), AugB: new Date('2024-08-02T14:12:34.000Z'), AugC: new Date('2024-08-03T04:12:34.000Z'), AugD: new Date('2024-08-03T14:12:34.000Z'), AugE: new Date('2024-08-04T04:12:34.000Z'), AugF: new Date('2024-08-04T14:12:34.000Z'), AugG: new Date('2024-08-05T04:12:34.000Z'), AugH: new Date('2024-08-05T14:12:34.000Z'), AugI: new Date('2024-08-06T04:12:34.000Z'), AugJ: new Date('2024-08-06T14:12:34.000Z'), AugK: new Date('2024-08-07T04:12:34.000Z'), AugL: new Date('2024-08-07T14:12:34.000Z'), AugM: new Date('2024-08-08T04:12:34.000Z'), AugN: new Date('2024-08-08T14:12:34.000Z'), OctA: new Date('2024-10-02T04:12:34.000Z'), OctB: new Date('2024-10-02T14:12:34.000Z'), OctC: new Date('2024-10-03T04:12:34.000Z'), OctD: new Date('2024-10-03T14:12:34.000Z'), OctE: new Date('2024-10-04T04:12:34.000Z'), OctF: new Date('2024-10-04T14:12:34.000Z'), OctG: new Date('2024-10-05T04:12:34.000Z'), OctH: new Date('2024-10-05T14:12:34.000Z'), OctI: new Date('2024-10-06T04:12:34.000Z'), OctJ: new Date('2024-10-06T14:12:34.000Z'), OctK: new Date('2024-10-07T04:12:34.000Z'), OctL: new Date('2024-10-07T14:12:34.000Z'), OctM: new Date('2024-10-08T04:12:34.000Z'), OctN: new Date('2024-10-08T14:12:34.000Z'), DecA: new Date('2024-12-02T04:12:34.000Z'), DecB: new Date('2024-12-02T14:12:34.000Z'), DecC: new Date('2024-12-03T04:12:34.000Z'), DecD: new Date('2024-12-03T14:12:34.000Z'), DecE: new Date('2024-12-04T04:12:34.000Z'), DecF: new Date('2024-12-04T14:12:34.000Z'), DecG: new Date('2024-12-05T04:12:34.000Z'), DecH: new Date('2024-12-05T14:12:34.000Z'), DecI: new Date('2024-12-06T04:12:34.000Z'), DecJ: new Date('2024-12-06T14:12:34.000Z'), DecK: new Date('2024-12-07T04:12:34.000Z'), DecL: new Date('2024-12-07T14:12:34.000Z'), DecM: new Date('2024-12-08T04:12:34.000Z'), DecN: new Date('2024-12-08T14:12:34.000Z'), } } const run_main = (method = 'all', tz) => new Promise(resolve => { let t0 = performance.now() // skip the auto all tests if I hardcoded them to speed up tests if (isAuto && method == 'all') { if (Object.keys(tzDataAll).length) {return resolve()} } if (isAuto) { let step = method == 'all' ? 1 : 2 let tzstep = ((aTZ.indexOf(tz) + 1)+'').padStart(3) +'/' + aTZ.length dom.results.innerHTML = 'step '+ step +' of 2 ['+ method +'] '+ spacer + s16 + tzstep + sc + ' '+ tz } //console.log(isAuto, 'running', tz) if (!isAuto) { if (tz == undefined) { tz = dom.timezones.value } else { try { dom.timezones.value = tz } catch(e) { console.log(e) } } } if (tz == 'undefined') {tz = undefined} //tz = 'America/Paramaribo' // temp let dates = oDates let testsAll = { 'default': [ [dates.JanE, ['F','L','M','S']], [dates.JanG, ['F','L','M','S']], [dates.MayA, ['F','L','M','S']], [dates.MayM, ['F','L','M','S']], [dates.JulF, ['F','L','M','S']], [dates.JulH, ['F','L','M','S']], [dates.SepC, ['F','L','M','S']], [dates.SepE, ['F','L','M','S']], [dates.NovD, ['F','L','M','S']], [dates.NovF, ['F','L','M','S']], // MAX: these add nothing /* // weekdays/times per month //[dates.JanA, ['F','L','M','S']], //[dates.JanB, ['F','L','M','S']], //[dates.JanC, ['F','L','M','S']], //[dates.JanD, ['F','L','M','S']], //[dates.JanF, ['F','L','M','S']], //[dates.JanH, ['F','L','M','S']], //[dates.JanI, ['F','L','M','S']], //[dates.JanJ, ['F','L','M','S']], //[dates.JanK, ['F','L','M','S']], //[dates.JanL, ['F','L','M','S']], //[dates.JanM, ['F','L','M','S']], //[dates.JanN, ['F','L','M','S']], //[dates.MayB, ['F','L','M','S']], //[dates.MayC, ['F','L','M','S']], //[dates.MayD, ['F','L','M','S']], //[dates.MayE, ['F','L','M','S']], //[dates.MayF, ['F','L','M','S']], //[dates.MayG, ['F','L','M','S']], //[dates.MayH, ['F','L','M','S']], //[dates.MayI, ['F','L','M','S']], //[dates.MayJ, ['F','L','M','S']], //[dates.MayK, ['F','L','M','S']], //[dates.MayL, ['F','L','M','S']], //[dates.MayN, ['F','L','M','S']], //[dates.JulA, ['F','L','M','S']], //[dates.JulB, ['F','L','M','S']], //[dates.JulC, ['F','L','M','S']], //[dates.JulD, ['F','L','M','S']], //[dates.JulE, ['F','L','M','S']], //[dates.JulG, ['F','L','M','S']], //[dates.JulI, ['F','L','M','S']], //[dates.JulJ, ['F','L','M','S']], //[dates.JulK, ['F','L','M','S']], //[dates.JulL, ['F','L','M','S']], //[dates.JulM, ['F','L','M','S']], //[dates.JulN, ['F','L','M','S']], //[dates.SepA, ['F','L','M','S']], //[dates.SepB, ['F','L','M','S']], //[dates.SepD, ['F','L','M','S']], //[dates.SepF, ['F','L','M','S']], //[dates.SepG, ['F','L','M','S']], //[dates.SepH, ['F','L','M','S']], //[dates.SepI, ['F','L','M','S']], //[dates.SepJ, ['F','L','M','S']], //[dates.SepK, ['F','L','M','S']], //[dates.SepL, ['F','L','M','S']], //[dates.SepM, ['F','L','M','S']], //[dates.SepN, ['F','L','M','S']], [dates.NovA, ['F','L','M','S']], [dates.NovB, ['F','L','M','S']], [dates.NovC, ['F','L','M','S']], [dates.NovE, ['F','L','M','S']], [dates.NovG, ['F','L','M','S']], [dates.NovH, ['F','L','M','S']], [dates.NovI, ['F','L','M','S']], [dates.NovJ, ['F','L','M','S']], [dates.NovK, ['F','L','M','S']], [dates.NovL, ['F','L','M','S']], [dates.NovM, ['F','L','M','S']], [dates.NovN, ['F','L','M','S']], // entire months [dates.FebA, ['F','L','M','S']], [dates.FebB, ['F','L','M','S']], [dates.FebC, ['F','L','M','S']], [dates.FebD, ['F','L','M','S']], [dates.FebE, ['F','L','M','S']], [dates.FebF, ['F','L','M','S']], [dates.FebG, ['F','L','M','S']], [dates.FebH, ['F','L','M','S']], [dates.FebI, ['F','L','M','S']], [dates.FebJ, ['F','L','M','S']], [dates.FebK, ['F','L','M','S']], [dates.FebL, ['F','L','M','S']], [dates.FebM, ['F','L','M','S']], [dates.FebN, ['F','L','M','S']], [dates.MarA, ['F','L','M','S']], [dates.MarB, ['F','L','M','S']], [dates.MarC, ['F','L','M','S']], [dates.MarD, ['F','L','M','S']], [dates.MarE, ['F','L','M','S']], [dates.MarF, ['F','L','M','S']], [dates.MarG, ['F','L','M','S']], [dates.MarH, ['F','L','M','S']], [dates.MarI, ['F','L','M','S']], [dates.MarJ, ['F','L','M','S']], [dates.MarK, ['F','L','M','S']], [dates.MarL, ['F','L','M','S']], [dates.MarM, ['F','L','M','S']], [dates.MarN, ['F','L','M','S']], [dates.AprA, ['F','L','M','S']], [dates.AprB, ['F','L','M','S']], [dates.AprC, ['F','L','M','S']], [dates.AprD, ['F','L','M','S']], [dates.AprE, ['F','L','M','S']], [dates.AprF, ['F','L','M','S']], [dates.AprG, ['F','L','M','S']], [dates.AprH, ['F','L','M','S']], [dates.AprI, ['F','L','M','S']], [dates.AprJ, ['F','L','M','S']], [dates.AprK, ['F','L','M','S']], [dates.AprL, ['F','L','M','S']], [dates.AprM, ['F','L','M','S']], [dates.AprN, ['F','L','M','S']], [dates.JunA, ['F','L','M','S']], [dates.JunB, ['F','L','M','S']], [dates.JunC, ['F','L','M','S']], [dates.JunD, ['F','L','M','S']], [dates.JunE, ['F','L','M','S']], [dates.JunF, ['F','L','M','S']], [dates.JunG, ['F','L','M','S']], [dates.JunH, ['F','L','M','S']], [dates.JunI, ['F','L','M','S']], [dates.JunJ, ['F','L','M','S']], [dates.JunK, ['F','L','M','S']], [dates.JunL, ['F','L','M','S']], [dates.JunM, ['F','L','M','S']], [dates.JunN, ['F','L','M','S']], [dates.AugA, ['F','L','M','S']], [dates.AugB, ['F','L','M','S']], [dates.AugC, ['F','L','M','S']], [dates.AugD, ['F','L','M','S']], [dates.AugE, ['F','L','M','S']], [dates.AugF, ['F','L','M','S']], [dates.AugG, ['F','L','M','S']], [dates.AugH, ['F','L','M','S']], [dates.AugI, ['F','L','M','S']], [dates.AugJ, ['F','L','M','S']], [dates.AugK, ['F','L','M','S']], [dates.AugL, ['F','L','M','S']], [dates.AugM, ['F','L','M','S']], [dates.AugN, ['F','L','M','S']], [dates.OctA, ['F','L','M','S']], [dates.OctB, ['F','L','M','S']], [dates.OctC, ['F','L','M','S']], [dates.OctD, ['F','L','M','S']], [dates.OctE, ['F','L','M','S']], [dates.OctF, ['F','L','M','S']], [dates.OctG, ['F','L','M','S']], [dates.OctH, ['F','L','M','S']], [dates.OctI, ['F','L','M','S']], [dates.OctJ, ['F','L','M','S']], [dates.OctK, ['F','L','M','S']], [dates.OctL, ['F','L','M','S']], [dates.OctM, ['F','L','M','S']], [dates.OctN, ['F','L','M','S']], [dates.DecA, ['F','L','M','S']], [dates.DecB, ['F','L','M','S']], [dates.DecC, ['F','L','M','S']], [dates.DecD, ['F','L','M','S']], [dates.DecE, ['F','L','M','S']], [dates.DecF, ['F','L','M','S']], [dates.DecG, ['F','L','M','S']], [dates.DecH, ['F','L','M','S']], [dates.DecI, ['F','L','M','S']], [dates.DecJ, ['F','L','M','S']], [dates.DecK, ['F','L','M','S']], [dates.DecL, ['F','L','M','S']], [dates.DecM, ['F','L','M','S']], [dates.DecN, ['F','L','M','S']], //*/ ], 'ethiopic': [ // 76bada35 (without the post minimums changes to default) // adding on top of default // ignore short + long: tested: adds nothing // note we only need one month: tested with january (but we'll include all for the sake of it) // note we're not testing every weekday per month; just using base gregory dates [dates.JanE, ['F','M']], [dates.JanG, ['F','M']], [dates.MayA, ['F','M']], [dates.MayM, ['F','M']], [dates.JulF, ['F','M']], [dates.JulH, ['F','M']], [dates.SepC, ['F','M']], [dates.SepE, ['F','M']], [dates.NovD, ['F','M']], [dates.NovF, ['F','M']], ], 'japanese': [ // 1de2b69f // adding on top of default // ignore short + long: tested: adds nothing // note we're not testing every weekday per month; just using base gregory dates [dates.JanE, ['F','M']], [dates.JanG, ['F','M']], [dates.MayA, ['F','M']], [dates.MayM, ['F','M']], [dates.JulF, ['F','M']], [dates.JulH, ['F','M']], [dates.SepC, ['F','M']], [dates.SepE, ['F','M']], [dates.NovD, ['F','M']], [dates.NovF, ['F','M']], ], // other calendars add nothing: tested: // buddhist, chinese, coptic, dangi, ethioaa, hebrew, indian, // islamic-civil, islamic-tbla, islamic-umalqura, iso8601, persian, roc // note we're not testing every weekday per month; just using base gregory dates } // matches let testsMin = { // get minimum as we add each calendar // then once done, see if we can reduce anything from previous calendars 'default': [ // this is pretty much it for gregory: can't improve and seems to always cover everything [dates.JanE, ['FM','ML']], [dates.JanG, ['FM','ML']], //[dates.MayA, ['FM']], // we can drop May now we have ethiopic + japanese minimums //[dates.MayM, ['FM']], [dates.JulF, ['FM','ML']], [dates.JulH, ['FM','ML']], [dates.SepC, ['FM','SF']], [dates.SepE, ['FM','SF']], //[dates.NovD, ['FM','ML']], // we can drop Nov 'FM' now we have ethiopic + japanese minimums //[dates.NovF, ['FM','ML']], [dates.NovD, ['ML']], [dates.NovF, ['ML']], ], 'ethiopic': [ // note: FM or MF combos don't cut it // looks like we need 1xF and 1xM across any dates(s): let's keep it simple with a single date [dates.JanE, ['F','M']], ], 'japanese': [ // looks like this is all we need [dates.SepC, ['M']], [dates.NovD, ['M']], // required for blink 147 ], } //testsMin = {} let tests = 'all' == method ? testsAll : testsMin let oConst = {} oData = {} let oOptions = { 'F': 'full', 'M': 'medium', 'L': 'long', 'S': 'short', 'FL': 'full_long', 'FM': 'full_medium', 'FS': 'full_short', 'LF': 'long_full', 'LS': 'long_short', 'LM': 'long_medium', 'MF': 'medium_full', 'ML': 'medium_long', 'MS': 'medium_short', 'SF': 'short_full', 'SL': 'short_long', 'SM': 'short_medium', } //aLocales = ['en','fr'] try { aLocales.forEach(function(code) { let oTempData = {} Object.keys(tests).sort().forEach(function(cal) { let src = cal // calendar in options really slows this down if ('all' == method) { if ('default' == cal) { try {oConst.DaF = Intl.DateTimeFormat(code, {dateStyle: 'full', timeStyle: 'full', timeZone: tz})} catch(e) {} try {oConst.DaM = Intl.DateTimeFormat(code, {dateStyle: 'medium', timeStyle: 'medium', timeZone: tz})} catch(e) {} try {oConst.DaL = Intl.DateTimeFormat(code, {dateStyle: 'long', timeStyle: 'long', timeZone: tz})} catch(e) {} try {oConst.DaS = Intl.DateTimeFormat(code, {dateStyle: 'short', timeStyle: 'short', timeZone: tz})} catch(e) {} // add calendar default name for visual info: not required for entropy //cal += '-'+ oConst.DaF.resolvedOptions().calendar /* th = buddhist ar-sa = islamic-umalqura ckb-ir, fa, fa-af, lrc, msn, ps, uz-arab = persian */ } else { try {oConst.DaF = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'full', timeStyle: 'full', timeZone: tz})} catch(e) {} try {oConst.DaM = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'medium', timeStyle: 'medium', timeZone: tz})} catch(e) {} try {oConst.DaL = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'long', timeStyle: 'long', timeZone: tz})} catch(e) {} try {oConst.DaS = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'short', timeStyle: 'short', timeZone: tz})} catch(e) {} } } else { if ('default' == cal) { /* debugging try {oConst.DaF = Intl.DateTimeFormat(code, {dateStyle: 'full', timeStyle: 'full', timeZone: tz})} catch(e) {} try {oConst.DaL = Intl.DateTimeFormat(code, {dateStyle: 'long', timeStyle: 'long', timeZone: tz})} catch(e) {} try {oConst.DaM = Intl.DateTimeFormat(code, {dateStyle: 'medium', timeStyle: 'medium', timeZone: tz})} catch(e) {} try {oConst.DaS = Intl.DateTimeFormat(code, {dateStyle: 'short', timeStyle: 'short', timeZone: tz})} catch(e) {} try {oConst.DaLS = Intl.DateTimeFormat(code, {dateStyle: 'long', timeStyle: 'short', timeZone: tz})} catch(e) {} try {oConst.DaMS = Intl.DateTimeFormat(code, {dateStyle: 'medium', timeStyle: 'short', timeZone: tz})} catch(e) {} try {oConst.DaFL = Intl.DateTimeFormat(code, {dateStyle: 'full', timeStyle: 'long', timeZone: tz})} catch(e) {} try {oConst.DaLF = Intl.DateTimeFormat(code, {dateStyle: 'long', timeStyle: 'full', timeZone: tz})} catch(e) {} try {oConst.DaSL = Intl.DateTimeFormat(code, {dateStyle: 'short', timeStyle: 'long', timeZone: tz})} catch(e) {} //*/ try {oConst.DaFM = Intl.DateTimeFormat(code, {dateStyle: 'full', timeStyle: 'medium', timeZone: tz})} catch(e) {} try {oConst.DaML = Intl.DateTimeFormat(code, {dateStyle: 'medium', timeStyle: 'long', timeZone: tz})} catch(e) {} try {oConst.DaSF = Intl.DateTimeFormat(code, {dateStyle: 'short', timeStyle: 'full', timeZone: tz})} catch(e) {} // add calendar default name for visual info: not required for entropy //cal += '-'+ oConst.DaFM.resolvedOptions().calendar } else { try {oConst.DaF = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'full', timeStyle: 'full', timeZone: tz})} catch(e) {} try {oConst.DaM = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'medium', timeStyle: 'medium', timeZone: tz})} catch(e) {} //try {oConst.DaMF = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'medium', timeStyle: 'full', timeZone: tz})} catch(e) {} //try {oConst.DaFM = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'full', timeStyle: 'medium', timeZone: tz})} catch(e) {} //try {oConst.DaMF = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'medium', timeStyle: 'full', timeZone: tz})} catch(e) {} //try {oConst.DaML = Intl.DateTimeFormat(code, {calendar: cal, dateStyle: 'medium', timeStyle: 'long', timeZone: tz})} catch(e) {} } } oTempData[cal] = {} let array = tests[src] array.forEach(function(item) { let date = item[0] let aStyles = item[1] aStyles.forEach(function(opt) { let k = oOptions[opt] // match TZP with friendly names if (oTempData[cal][k] == undefined) {oTempData[cal][k] = []} let constructor = 'Da'+ opt let formatter = oConst[constructor] try { oTempData[cal][k].push(formatter.format(date)) } catch(e) { oTempData[cal][k].push('error') } }) }) }) //console.log(oTempData) let hash = mini(oTempData) if (oData[hash] == undefined) { oData[hash] = {} oData[hash]['locales'] = [code] oData[hash]['data'] = {} for (const k of Object.keys(oTempData).sort()) { oData[hash]['data'][k] = oTempData[k] } } else { oData[hash]['locales'].push(code) } }) // perf if (!isAuto) { dom.perf.innerHTML = Math.round(performance.now() - t0) +' ms' //console.log(oData) } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]['locales']) let localeCount = oData[k]['locales'].length if (!isAuto) { let str = '' for (const p of Object.keys(oData[k]['data'])) { str += s14 + p +': '+ sc for (const style of Object.keys(oData[k]['data'][p]).sort()) { let linedata = "<li class='dates'>"+ oData[k]["data"][p][style].join("</li><li class='dates'>") + "</li>" str += "<li>"+ s16 + style.toLowerCase() +": "+ sc + linedata +"</li>" } } displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"].join(", ") +"</li></ul>" ) } } // hashes + btns let resultsBtn = '', localesBtn = '' localeGroups.sort() if (!isAuto) { sDetail['results'] = oData resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" sDetail['locales'] = localeGroups localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" } let resultsHash = mini(oData) let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = '' if ('all' == method) { localesHashAll = localesHash // don't notate anything: the numbers can change depending on your timezone } else if ('min' == method) { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } if (tz !== undefined) { if (tzData[tz] == undefined) { tzData[tz] = {} } let buckets = localeGroups.length if ('all' == method) { if (isMax == undefined) { isMax = buckets isMin = buckets } else { if (buckets > isMax) {isMax = buckets} if (buckets < isMin) {isMin = buckets} } } tzData[tz][method] = [buckets, localesHash] } if (!isAuto) { // display let display = s8 +'timeZone: '+ sc + tz + spacer + s4 + localeGroups.length + sc +' from '+ s4 + aLocales.length + sc + spacer + s16 +'results: '+ sc + resultsHash +' ' + resultsBtn +'<br>' + s12 +'locales: '+ sc + localesHash +' '+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + '<br>' + displaylist.join('<br>') } return resolve() } catch(e) { dom.results.innerHTML = s4 + e.name +': '+ sc + e.message return resolve() } }) function run_summary() { // clear isAuto isAuto = false let aCheck = [] for (const k of Object.keys(tzData).sort()) { aCheck.push(k +': '+ tzData[k].all[0]) if (tzData[k].min == undefined) { oCheck['missing'].push(k) } else { let strAll = tzData[k]['all'].join(', ') let strMin = tzData[k]['min'].join(', ') if (strAll == strMin) { oCheck['match'].push(k +': ' + strAll) } else { strAll = s14 + tzData[k]['all'][0] + sc +' ['+ tzData[k]['all'][1] +']' strMin = s14 + tzData[k]['min'][0] + sc +' ['+ tzData[k]['min'][1] +']' oCheck['mismatch'].push(k +': '+ strAll +' vs ' + strMin) } } } oCheck['max'] = isMax oCheck['min'] = isMin let display = [] let count = oCheck['match'].length let expected = aTZ.length display.push(s14 +'MATCH: '+ sc + count +'/'+ expected) count = oCheck['missing'].length if (oCheck['missing'].length) { display.push('<br>' + s14 +'MISSING: '+ sc + count +'<br>') display.push(oCheck['missing'].join('<br>')) } count = oCheck['mismatch'].length if (oCheck['mismatch'].length) { display.push('<br>' + s14 +'MISMATCH: '+ sc + count +'<br>') display.push(oCheck['mismatch'].join('<br>')) } display.push('<br>' + s14 +'RANGE: '+ sc + isMin +' - '+ isMax) dom.results.innerHTML = s16 + 'ALL vs MIN' + sc + spacer + display.join('<br>') + spacer +'<hr><br>'+ s16 +'ENTROPY:'+ sc +' (number of locale results per timezone)' + sc + ' '+ mini(aCheck) + spacer + aCheck.join('<br>') + spacer console.log(oCheck) console.log(mini(aCheck), '\n', aCheck) } const run_both = (tz) => new Promise(resolve => { setTimeout(function(){ Promise.all([ run_main('all', tz) ]).then(function(){ setTimeout(function(){ Promise.all([ run_main('min', tz) ]).then(function(){ return resolve() }) }, 1) }) }, 1) }) function run_all() { // prevent user runs isAuto = true // reset buttons setBtn() // clear displays dom.perf = '' dom.results = '' // reset data oCheck = {'match': [], 'max': 0, 'min': 0, 'mismatch': [], 'missing': [] } tzData = {} isMax = undefined isMin = undefined // speed up tesys by using a hardcoded 'all' if (Object.keys(tzDataAll).length) { for (const k of Object.keys(tzDataAll).sort()) { tzData[k] = {} tzData[k]['all'] = tzDataAll[k]['all'] } } // loop timezones for (let i=0; i < aTZ.length; i++) { let tz = aTZ[i] Promise.all([ run_both(tz) ]).then(function(){ if (i == (aTZ.length - 1)) { run_summary() } }) } } function run(method) { if (isAuto) { // do not allow user clicks to interfer with an auto run return } if (isSupported) { if (method == 'next') { method = 'all' let target = dom.timezones target.selectedIndex++ if (target.value == '') {target.selectedIndex++} // end of list, return to top } //reset setBtn(method) dom.perf = '' dom.results = '' // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName('btn8') for (let i=0; i < items.length; i++) { items[i].classList.add('btn4') items[i].classList.remove('btn8') } if (!isAuto) { // set btn let el = document.getElementById('b'+ method) el.classList.add('btn8') el.classList.remove('btn4') } } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { // pointless if we can't use the feature being tested: FF58+ let formatF = new Intl.DateTimeFormat('en', {dateStyle: 'full', timeStyle: 'full'}) let formatS = new Intl.DateTimeFormat('en', {dateStyle: 'short', timeStyle: 'short'}) let testDate = new Date('January 5, 2024') if (formatF.format(testDate) !== formatS.format(testDate)) { isSupported = true } else { dom.results.innerHTML = s4 + 'dateStyle | timeStyle:' + sc + ' not supported' } } catch(e) { dom.results.innerHTML = s4 + e.name +':' + sc +' '+ e.message } // add additional locales to core locales for this test let aListExtra = [ 'af-na,afrikaans (namibia)', 'ar-ae,arabic (united arabic emirates)', 'ar-dz,arabic (algeria)', 'ar-il,arabic (israel)', 'ar-iq,arabic (iraq)', 'ar-km,arabic (cosmoros)', 'ar-ma,arabic (morocco)', 'ar-mr,arabic (mauritania)', 'ar-sa,arabic (saudi arabia)', 'az-cyrl,azerbaijani (cyrillic)', 'bn-in,bengali (india)', 'bo-in,tibetan (india)', 'bs-cyrl,bosnian (cyrillic)', 'ckb-ir,central kurdish (iran)', 'de-at,german (austria)', 'de-ch,german (switzerland)', 'ee-tg,éwé (togo)', 'en-001,english', 'en-150,english (europe)', 'en-ae,english (united arab emirates)', 'en-au,english (australia)', 'en-be,english (belgium)', 'en-bi,english (burundi)', 'en-bw,english (botswana)', 'en-bz,english (belize)', 'en-ca,english (canada)', 'en-ch,english (switzerland)', 'en-dk,english (denmark)', 'en-er,english (eritrea)', 'en-fi,english (finland)', 'en-gb,english (united kingdom)', 'en-gu,english (guam)', 'en-gy,english (guyana)', 'en-hk,english (hong kong)', 'en-ie,english (ireland)', 'en-il,english (israel)', 'en-in,english (india)', 'en-ke,english (kenya)', 'en-mh,english (marshall islands)', 'en-mo,english (macau)', 'en-mt,english (malta)', 'en-my,english (malaysia)', 'en-nz,english (new zealand)', 'en-pk,english (pakistan)', 'en-se,english (sweden)', 'en-sg,english (singapore)', 'en-za,english (south africa)', 'en-zw,english (zimbabwe)', 'es-ar,spanish (argentina)', 'es-419,spanish (latin america and the caribbean)', 'es-bo,spanish (bolivia)', 'es-br,spanish (brazil)', 'es-cl,spanish (chile)', 'es-co,spanish (colombia)', 'es-do,spanish (dominican republic)', 'es-ec,spanish (ecuador)', 'es-gt,spanish (guatemala)', 'es-hn,spanish (honduras)', 'es-mx,spanish (mexico)', 'es-pa,spanish (panama)', 'es-pe,spanish (peru)', 'es-ph,spanish (philippines)', 'es-py,spanish (paraguay)', 'es-us,spanish (united states)', 'es-uy,spanish (uruguay)', 'es-ve,spanish (venezuela)', 'fa-af,persian (afghanistan)', 'ff-adlm,fulah (adlam)', 'ff-adlm-gh,fulah (adlam ghana)', 'ff-gh,fulah (ghana)', 'ff-mr,fulah (mauritania)', 'fr-be,french (belgium)', 'fr-ca,french (canada)', 'fr-ch,french (switzerland)', 'fr-dj,french (djibouti)', 'fr-gf,french (french guiana)', 'fr-ma,french (morocco)', 'fr-ml,french (mali)', 'ha-gh,hausa (ghana)', 'hi-latn,hindi (latin)', 'hr-ba,croatian (bosnia & herzegovina)', 'it-ch,italian (switzerland)', 'kk-cn,kazakh (china)', 'ko-kp,korean (north korea)', 'kok-in,konkani (india)', 'kok-latn,konkani (latin)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'lrc-iq,northern luri (iraq)', 'ms-bn,malay (brunei)', 'ms-id,malay (indonesia)', 'ne-in,nepali (india)', 'nl-be,dutch (belgium)', 'nl-sr,dutch (suriname)', 'om-ke,oromo (kenya)', 'pa-arab,punjabi (arabic)', 'ps-pk,pashto (pakistan)', 'pt-ao,portuguese (angola)', 'pt-ch,portuguese (switzerland)', 'pt-mo,portuguese (macau)', 'qu-bo,quechua (bolivia)', 'qu-ec,quechua (ecuador)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'shi-latn,tachelhit (latin)', 'so-ke,somali (kenya)', 'sq-mk,albanian (macedonia)', 'sr-ba,serbian (bosnia & herzegovina)', 'sr-cyrl-me,serbian (cyrillic montenegro)', 'sr-cyrl-xk,serbian (cyrillic kosovo)', 'sr-latn,serbian (latin)', 'sr-latn-ba,serbian (latin bosnia & herzegovina)', 'sr-latn-me,serbian (latin montenegro)', 'sr-latn-xk,serbian (latin kosovo)', 'st-ls,southern sotho', 'sv-ax,swedish (åland islands)', 'sv-fi,swedish (finland)', 'sw-ke,swahili (kenya)', 'ta-lk,tamil (sri lanka)', 'ta-my,tamil (malaysia)', 'ti-er,tigrinya (eritrea)', 'tr-cy,turkish (cyprus)', 'ur-in,urdu (india)', 'uz-arab,uzbek (arabic)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'vai-latn,vai (latin)', 'yo-bj,yoruba (benin)', 'yue-cn,cantonese (china)', 'zh-hans-hk,chinese (simplified hong kong)', 'zh-hans-mo,chinese (simplified macau)', 'zh-my,chinese (malaysia)', // blink 'ar-bh,arabic (bahrain)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['ar-sa,arabic (saudi arabia)','en,english','fa,persian','th,thai',] //list = ['en,english'] //list = ['pt-ao','pt-ch'] // e.g. split by europe/vatican legend() if (isSupported) { set_dates() setBtn('all') setTimeout(function() { run_main('all') }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dtfdayperiod.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>dft: dayperiod</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <!-- custom --> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2"> <div class="nav-title">datetimeformat: dayperiod <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy: from three options (<code>narrow</code>, <code>short</code>, <code>long</code>) and five times (<code>8:00</code>, <code>12:00</code>, <code>15:00</code>, <code>18:00</code>, <code>22:00</code>). Click custom to test any configuration.</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="bnarrow" class="btn4 btnfirst" onClick="run('narrow')">[ N ]</span> <span id="bshort" class="btn4 btn" onClick="run('short')">[ S ]</span> <span id="blong" class="btn4 btn" onClick="run('long')">[ L ]</span> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bcustom" class="btn4 btn" onClick="run('custom')">[ CUSTOM ]</span> <br><br><hr><br> <span class="hidden" id ="customoptions"> <p class="mono spaces pad">NARROW: 8 <input type="checkbox" id="narrow08"> 12 <input type="checkbox" id="narrow12"> 15 <input type="checkbox" id="narrow15"> 18 <input type="checkbox" id="narrow18"> 22 <input type="checkbox" id="narrow22"></p> <p class="mono spaces pad"> SHORT: 8 <input type="checkbox" id="short08"> 12 <input type="checkbox" id="short12"> 15 <input type="checkbox" id="short15"> 18 <input type="checkbox" id="short18"> 22 <input type="checkbox" id="short22"></p> <p class="mono spaces pad"> LONG: 8 <input type="checkbox" id="long08"> 12 <input type="checkbox" id="long12"> 15 <input type="checkbox" id="long15"> 18 <input type="checkbox" id="long18"> 22 <input type="checkbox" id="long22"></p> <span class="btn4 btnfirst" onClick="reset_custom('clear')">[ CLEAR ]</span> <span class="btn4 btn" onClick="reset_custom('min')">[ RESET MIN ]</span> <span class="btn4 btn" onClick="reset_custom('all')">[ ALL ]</span> <span class="btn4 btn" onClick="run_custom()">[ RUN ]</span> <br><br><hr><br> </span> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = "" // to compare custom to const oDays = { "08": new Date("2019-01-30T08:00:00Z"), "12": new Date("2019-01-30T12:00:00Z"), "15": new Date("2019-01-30T15:00:00Z"), "18": new Date("2019-01-30T18:00:00Z"), "22": new Date("2019-01-30T22:00:00Z") } function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function reset_custom(type) { // check/uncheck everything let checkState = (type == "all") let styles = ["narrow","short","long"], times = ["08",12,15,18,22] styles.forEach(function(s){ times.forEach(function(t){ document.getElementById(s +t).checked = checkState }) }) if (type == "min") { // gecko min preset dom.narrow08.checked = true dom.long08.checked = true dom.short12.checked = true dom.narrow15.checked = true dom.short18.checked = true // assuming 110 changes stick from ICU 72: 1792775 if (isFF && "object" !== typeof ondeviceorientationabsolute) { // FF90-109 dom.short22.checked = true } else { // FF110+, also chrome since at least 115 dom.short15.checked = true dom.long22.checked = true } } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let test = Intl.DateTimeFormat.supportedLocalesOf([code]) if (test.length) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function get_dayperiod(date, code, option) { // always use h12 return new Intl.DateTimeFormat(code, {hourCycle: "h12", timeZone: 'UTC', dayPeriod: option}).format(date) } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" try { let styles = ["narrow","short","long"] let times = ["08",12,15,18,22] let oOptions = {} // select what to test if (method == "custom") { styles.forEach(function(s){ times.forEach(function(t){ oOptions[s + t] = document.getElementById(s +t).checked }) }) } else { if (method == "narrow") {styles = ["narrow"]} if (method == "short") {styles = ["short"]} if (method == "long") {styles = ["long"]} styles.forEach(function(s){ times.forEach(function(t){ oOptions[s + t] = true }) }) } // test: 3 methods x 5 dayPeriods aLocales.forEach(function(code) { // set our three options once per code let dteS = new Intl.DateTimeFormat(code, {hourCycle: "h12", timeZone: 'UTC', dayPeriod: "short"}), dteN = new Intl.DateTimeFormat(code, {hourCycle: "h12", timeZone: 'UTC', dayPeriod: "narrow"}), dteL = new Intl.DateTimeFormat(code, {hourCycle: "h12", timeZone: 'UTC', dayPeriod: "long"}) let oStyles = {"narrow": [], "short": [], "long": []} styles.forEach(function(s){ times.forEach(function(t){ if (oOptions[s + t] == true) { if (s == "short") { oStyles[s].push(dteS.format(oDays[t])) } else if (s =="narrow") { oStyles[s].push(dteN.format(oDays[t])) } else { oStyles[s].push(dteL.format(oDays[t])) } } }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] oTempData[hash]["long"] = oStyles["long"].join(" | ") oTempData[hash]["short"] = oStyles["short"].join(" | ") oTempData[hash]["narrow"] = oStyles["narrow"].join(" | ") } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == "locales") { oData[n][p] = oTempData[n][p].join(", ") } else { oData[n][p] = oTempData[n][p] } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let strN = oData[k]["narrow"], strS = oData[k]["short"], strL = oData[k]["long"], localeCount = oData[k]["locales"].split(",").length if (strN.length) {strN = "<li>"+ s16 +"N: "+ sc + strN +"</li>"} if (strS.length) {strS = "<li>"+ s16 +"S: "+ sc + strS +"</li>"} if (strL.length) {strL = "<li>"+ s16 +"L: "+ sc + strL +"</li>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>" + strN + strS + strL + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "2fb81fcd") { // FF151+ } else if (resultsHash == "5acc9006") { // FF147-150 } else if (resultsHash == "aaff42db") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "8d237b4d") { // FF147+: 227 } else if (localesHash == "0d1d591e") { // FF140+: 225 } else {localesHash += ' '+ zNEW } } } else if (method == "custom") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" "+ resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" let element = dom.customoptions if (method == "custom") { // unhide custom section element.classList.remove("hidden") } else { // hide custom element.classList.add("hidden") dom.results = "running test..." // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } } function run_custom() { if (isSupported) { //reset dom.perf = "" dom.results = "running test..." setTimeout(function() { run_main("custom") }, 1) } } function run_test() { let A = get_dayperiod(new Date("2019-01-30T08:00:00Z"), "en", "long") let B = get_dayperiod(new Date("2019-01-30T12:00:00Z"), "en", "long") if (A == B) { isSupported = false dom.results.innerHTML = s4 + "dayPeriod:" + sc + " not supported" } else { isSupported = true setBtn("all") dom.results = "running test..." setTimeout(function() { run_main("all") }, 100) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() reset_custom("min") // add additional locales to core locales for this test let aListExtra = [ "az-cyrl,azerbaijani (cyrillic)", "bn-in,bengali (india)", "bs-cyrl,bosnian (cyrillic)", "en-au,english (australia)", "en-ca,english (canada)", "es-ar,spanish (argentina)", "es-co,spanish (colombia)", "es-do,spanish (dominican republic)", "es-mx,spanish (mexico)", "es-pa,spanish (panama)", "fa-af,persian (afghanistan)", "ff-adlm,fulah (adlam)", "fr-ca,french (canada)", "fr-ch,french (switzerland)", "fr-sn,french (senegal)", "hi-latn,hindi (latin)", 'kk-cn,kazakh (china)', "kok-latn,konkani (latin)", "ks-deva,kashmiri (devanagari)", "kxv-telu,kuvi (telugu)", "pt-pt,portuguese (portugal)", "ro-md,romanian (moldova)", "sd-deva,sindhi (devanagari)", "se-fi,northern sami (finland)", "shi-latn,tachelhit (latin)", "sr-cyrl-ba,serbian (cyrillic bosnia & herzegovina)", "sr-cyrl-me,serbian (cyrillic montenegro)", "sr-cyrl-xk,serbian (cyrillic kosovo)", "sr-latn,serbian (latin)", "sr-latn-ba,serbian (latin bosnia & herzegovina)", "sr-latn-me,serbian (latin montenegro)", "sr-latn-xk,serbian (latin kosovo)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "yo-bj,yoruba (benin)", "yue-hans,cantonese (simplified)", ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() run_test() }) </script> </body> </html> ================================================ FILE: tests/dtflistformat.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>dtf: listformat</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">datetimeformat: listformat <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy: from three styles (<code>narrow</code>, <code>short</code>, <code>long</code>) and three types (<code>conjunction</code>, <code>disjunction</code>, <code>unit</code>). Click custom to test any configuration.</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="bnarrow" class="btn4 btnfirst" onClick="run('narrow')">[ N ]</span> <span id="bshort" class="btn4 btn" onClick="run('short')">[ S ]</span> <span id="blong" class="btn4 btn" onClick="run('long')">[ L ]</span> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bcustom" class="btn4 btn" onClick="run('custom')">[ CUSTOM ]</span> <br><br><hr><br> <span class="hidden" id ="customoptions"> <p class="mono spaces">NARROW: conjunction <input type="checkbox" id="narrowconjunction"> disjunction <input type="checkbox" id="narrowdisjunction"> unit <input type="checkbox" id="narrowunit"></p> <p class="mono spaces"> SHORT: conjunction <input type="checkbox" id="shortconjunction"> disjunction <input type="checkbox" id="shortdisjunction"> unit <input type="checkbox" id="shortunit"></p> <p class="mono spaces"> LONG: conjunction <input type="checkbox" id="longconjunction"> disjunction <input type="checkbox" id="longdisjunction"> unit <input type="checkbox" id="longunit"></p> <span class="btn4 btnfirst" onClick="reset_custom('clear')">[ CLEAR ]</span> <span class="btn4 btn" onClick="reset_custom('min')">[ RESET MIN ]</span> <span class="btn4 btn" onClick="reset_custom('all')">[ ALL ]</span> <span class="btn4 btn" onClick="run_custom()">[ RUN ]</span> <br><br><hr><br> </span> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = "" // to compare custom to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function reset_custom(type) { // check/uncheck everything let checkState = (type == "all") let styles = ["narrow","short","long"], types = ["conjunction","disjunction","unit"] styles.forEach(function(s){ types.forEach(function(t){ document.getElementById(s +t).checked = checkState }) }) if (type == "min") { // min preset dom.narrowunit.checked = true dom.narrowconjunction.checked = true dom.narrowdisjunction.checked = true dom.longconjunction.checked = true dom.shortunit.checked = true } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DateTimeFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function get_dayperiod(date, code, option) { // always use h12 return new Intl.DateTimeFormat(code, {hourCycle: "h12", dayPeriod: option}).format(date) } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" try { let styles = ['narrow','short','long'] let types = ['conjunction','disjunction','unit'] let oOptions = {} // select what to test if (method == "custom") { styles.forEach(function(s){ types.forEach(function(t){ oOptions[s + t] = document.getElementById(s +t).checked }) }) } else { if (method == "narrow") {styles = ["narrow"]} if (method == "short") {styles = ["short"]} if (method == "long") {styles = ["long"]} styles.forEach(function(s){ types.forEach(function(t){ oOptions[s + t] = true }) }) } //test: 3 styles x 3 types aLocales.forEach(function(code) { let oStyles = {"narrow": [], "short": [], "long": []} styles.forEach(function(s){ types.forEach(function(t){ if (oOptions[s + t] == true) { oStyles[s].push(new Intl.ListFormat(code, {style: s, type: t}).format(["a","b","c"])) } }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] oTempData[hash]["long"] = oStyles["long"].join(" | ") oTempData[hash]["short"] = oStyles["short"].join(" | ") oTempData[hash]["narrow"] = oStyles["narrow"].join(" | ") } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == "locales") { oData[n][p] = oTempData[n][p].join(", ") } else { oData[n][p] = oTempData[n][p] } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let strN = oData[k]["narrow"], strS = oData[k]["short"], strL = oData[k]["long"], localeCount = oData[k]["locales"].split(",").length if (strN.length) {strN = "<li>"+ s16 +"N: "+ sc + strN +"</li>"} if (strS.length) {strS = "<li>"+ s16 +"S: "+ sc + strS +"</li>"} if (strL.length) {strL = "<li>"+ s16 +"L: "+ sc + strL +"</li>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>" + strN + strS + strL + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "9c5d9625") { // FF147+ } else if (resultsHash == "324524af") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "6c202f92") { // FF147+: 157 } else if (localesHash == "4aff24fd") { // FF140-146: 153 } else {localesHash += ' '+ zNEW } } } else if (method == "custom") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" let element = dom.customoptions if (method == "custom") { // unhide custom section element.classList.remove("hidden") } else { // hide custom element.classList.add("hidden") dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } } function run_custom() { if (isSupported) { //reset dom.perf = "" dom.results = "" setTimeout(function() { run_main("custom") }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // support try { let test = new Intl.ListFormat(undefined, {style: "short", type: "conjunction"}).format(["a","b","c"]) isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } reset_custom("min") // add additional locales to core locales for this test let aListExtra = [ "bs-cyrl,bosnian (cyrillic)", "en-in,english (india)", "en-sg,english (singapore)", "es-do,spanish (dominican republic)", "es-us,spanish (united states)", "ff-adlm,fulah (adlam)", "hi-latn,hindi (latin)", 'kk-cn,kazakh (china)', 'kok-latn,konkani (latin)', "ks-deva,kashmiri (devanagari)", "kxv-telu,kuvi (telugu)", "pt-pt,portuguese (portugal)", "yo-bj,yoruba (benin)", // blink 'sr-latn,serbian (latin)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dtfrelated.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>dtf: relatedyear</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2"> <div class="nav-title">datetimeformat: relatedyear <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy with DateTimeFormat relatedYear</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = "", // to compare min to oData = {}, oCompare = {'false': [], 'true': []}, // check to*String matches INTL checkLocaleStrings = false function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DateTimeFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method = "all") { let t0 = performance.now() let spacer = "<br><br>" // all dates (days/months/am-pm) must be timezone resistent: we are checking locale only // timezonename (and locale) is tested in the a different PoC - see TZP set_oIntlDateTests section // thus we use UTC time so everyone uses the exact same dates, and then we pass // UTC as the timezone so nothing shifts, preserving our specific datetimes let dateA = new Date("January 5, 2023 1:00:00"), dateB = new Date(-1,12,5,1) let RY1 = new Date('-000002-01-15T01:00:00.000Z'), RY2 = new Date('2023-01-15T00:00:00.000Z') let datesAll = [RY1, RY2] let testsAll = {} let testsAllMini = { '2-digit': datesAll, 'long': datesAll, 'narrow': datesAll, 'numeric': datesAll, 'short': datesAll, } let aSupportedCal = [] try { aSupportedCal = Intl.supportedValuesOf('calendar') } catch(e) {} // which calendars don't do much // note: F147+ 1955545 Use "islamic-tbla" when either "islamic" or "islamic-rgsa" was requested let aCalendars = [ 'buddhist', // yes 'chinese', // yes 'coptic', 'default', 'gregory', // yes 'hebrew', // yes 'indian', // yes 'islamic-tbla', 'japanese', 'roc', // blink // these seem redundant but include them for the full test 'dangi', 'ethioaa', 'ethiopic', 'islamic', 'islamic-civil', 'islamic-rgsa', 'islamic-umalqura', 'iso8601', 'persian', ] aCalendars = aCalendars.filter(x => aSupportedCal.includes(x)) testsAll = {'default': testsAllMini} if (aCalendars.length) { aCalendars.forEach(function(cal) {testsAll[cal] = testsAllMini}) } let testsMin = { 'buddhist': {'long': [RY1]}, 'chinese': {'long': [RY1]}, 'coptic': {'long': [RY2]}, 'default': {'long': [RY1]}, 'gregory': {'long': [RY1]}, 'hebrew': {'long': [RY1]}, 'indian': {'long': [RY1]}, 'islamic-tbla': {'long': [RY1]}, 'japanese': {'long': datesAll}, // only one to use both 'roc': {'long': [RY1]}, } let tests = method == "all" ? testsAll : testsMin oData = {} oCompare = {'false': [], 'true': []} //aLocales = ['en'] try { aLocales.forEach(function(code) { let oTempData = {} Object.keys(tests).sort().forEach(function(cal) { oTempData[cal] = {} Object.keys(tests[cal]).forEach(function(style) { oTempData[cal][style] = [] let calname = 'default' == cal ? undefined : cal let formatter = Intl.DateTimeFormat(code, {calendar: cal, relatedYear: style, timeZone: 'UTC'}) tests[cal][style].forEach(function(d) { oTempData[cal][style].push(formatter.format(d)) if (checkLocaleStrings) { //* test toLocaleString let intlvalue = formatter.format(d) let strlocale = (d).toLocaleString(code, {calendar: cal, day: 'numeric', month: 'numeric', year: 'numeric', timeZone: 'UTC'}) let isMatch = intlvalue == strlocale oCompare[isMatch].push([intlvalue, strlocale]) } }) }) }) //console.log(oTempData) let hash = mini(oTempData) if (oData[hash] == undefined) { oData[hash] = {} oData[hash]["locales"] = [code] oData[hash]["data"] = {} for (const k of Object.keys(oTempData).sort()) { oData[hash]["data"][k] = oTempData[k] } } else { oData[hash]["locales"].push(code) } }) if (checkLocaleStrings) {console.log(oCompare)} // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" //console.log(oData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].length let str = '' for (const cal of Object.keys(oData[k]["data"])) { if ('min' == method) {str += '<ul class="main"><li>'} str += s14 + cal +": "+ sc for (const style of Object.keys(oData[k]["data"][cal]).sort()) { let shortstyle if (style == "numeric") {shortstyle = "Num"} else {shortstyle = style.slice(0,1).toUpperCase()} let tmpStr = s16 + shortstyle +": "+ sc + oData[k]["data"][cal][style].join(", ") if ('all' == method) { str += '<ul class="main"><li>'+ tmpStr +"</li>" } else { str += ' '+ tmpStr } if ('min' == method) {str += '</li>'} str += '</ul>' } } if (method == 'all') {str = '<details>'+ str +'</details>'} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"].join(", ") +"</li></ul>" ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // ignore if non-supported used, which return same as undefined = user's resolved options // results if (resultsHash == "6f15efe5") { // FF151+ } else if (resultsHash == "fddaa32a") { // FF147-150 } else if (resultsHash == "4b451618") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "e57bb0b6") { // FF151+: 226 } else if (localesHash == "bd60b370") { // FF147-150: 174 } else if (localesHash == "51160b4d") { // FF140-146: 171 } else {localesHash += ' '+ zNEW } } } else { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + localeGroups.length + sc +" from "+ s4 + aLocales.length + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" let msg = method == 'all' ? 'running... it takes a few seconds' : '' dom.results = msg // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() try { // pointless if we can't use the feature being tested: FF58+ let test = new Intl.DateTimeFormat("en").formatToParts(new Date) isSupported = true dom.results = 'running... it takes a few seconds' } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // add additional locales to core locales for this test let aListExtra = [ 'ar-bh,arabic (bahrain)', 'az-cyrl,azerbaijani (cyrillic)', 'bn-in,bengali (india)', 'bs-cyrl,bosnian (cyrillic)', 'ckb-ir,central kurdish (iran)', 'en-001,english', 'en-ae,english (united arab emirates)', 'en-au,english (australia)', 'en-be,english (belgium)', 'en-bw,english (botswana)', 'en-ca,english (canada)', 'en-ch,english (switzerland)', 'en-gb,english (united kingdom)', 'en-hk,english (hong kong)', 'en-in,english (india)', 'en-nz,english (new zealand)', 'en-se,english (sweden)', 'en-za,english (south africa)', 'es-ar,spanish (argentina)', 'es-cl,spanish (chile)', 'es-pa,spanish (panama)', 'es-pr,spanish (puerto rico)', 'fa-af,persian (afghanistan)', 'ff-adlm,fulah (adlam)', 'fr-ca,french (canada)', 'fr-ch,french (switzerland)', 'hi-latn,hindi (latin)', 'kk-cn,kazakh (china)', 'kok-latn,konkani (latin)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'lrc-iq,northern luri (iraq)', 'nl-be,dutch (belgium)', 'pa-arab,punjabi (arabic)', 'ps-pk,pashto (pakistan)', 'pt-ao,portuguese (angola)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'shi-latn,tachelhit (latin)', 'sr-latn,serbian (latin)', 'sv-fi,swedish (finland)', 'ur-in,urdu (india)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'yue-cn,cantonese (china)', 'zh-hans-hk,chinese (simplified hong kong)', // blink //* 'ar-sa,arabic (saudi arabia)', 'uz-af,uzbek (afghanistan)', 'uz-arab,uzbek (arabic)', //*/ ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() if (isSupported) { setBtn('all') setTimeout(function() { run('all') }, 100) } }) </script> </body> </html> ================================================ FILE: tests/dtftimezonename.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>dtf: timezonename</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 680px; max-width: 780px;} ul.main {margin-left: -20px;} div.nav-down {width: 450px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">datetimeformat: timezonename <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="c perf" id="timezone"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A firefox (128+) proof to confirm minimum tests for maximum entropy in timeZoneName. The default test covers base languages, but you can expand locales to expose <span class="s4">regional differences</span> on all three tests. The <code>TINY</code> test is a minimalist hardcoded test built for speed. You can also test individual timezones via the console; e.g. <code>run("Asia/Seoul")</code> </span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <span id="btiny" class="btn4 btn" onClick="run('tiny')">[ TINY ]</span> <input type="checkbox" id="optExpanded"> expand locales <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; let tzIgnore = [ // MISC: acronyms and old-timey aliases: adds nothing "CET","CST6CDT","Cuba","EET","EST","EST5EDT","Egypt","Eire","Factory","GB","GB-Eire", "GMT","GMT+0","GMT-0","GMT0","Greenwich","HST","Hongkong","Iceland","Iran","Israel", "Jamaica","Japan","Kwajalein","Libya","MET","MST","MST7MDT","NZ","NZ-CHAT","Navajo", "PRC","PST8PDT","Poland","Portugal","ROC","ROK","Singapore","Turkey","UCT","UTC", "Universal","W-SU","WET","Zulu", "Brazil/Acre","Brazil/DeNoronha","Brazil/East","Brazil/West","Canada/Atlantic", "Canada/Central","Canada/Eastern","Canada/Mountain","Canada/Newfoundland","Canada/Pacific", "Canada/Saskatchewan","Canada/Yukon","Chile/Continental","Chile/EasterIsland", "Etc/GMT","Etc/GMT+0","Etc/GMT+1","Etc/GMT+10","Etc/GMT+11","Etc/GMT+12","Etc/GMT+2", "Etc/GMT+3","Etc/GMT+4","Etc/GMT+5","Etc/GMT+6","Etc/GMT+7","Etc/GMT+8","Etc/GMT+9", "Etc/GMT-0","Etc/GMT-1","Etc/GMT-10","Etc/GMT-11","Etc/GMT-12","Etc/GMT-13","Etc/GMT-14", "Etc/GMT-2","Etc/GMT-3","Etc/GMT-4","Etc/GMT-5","Etc/GMT-6","Etc/GMT-7","Etc/GMT-8","Etc/GMT-9", "Etc/GMT0","Etc/Greenwich","Etc/UCT","Etc/UTC","Etc/Universal","Etc/Zulu", "Mexico/BajaNorte","Mexico/BajaSur","Mexico/General","US/Alaska","US/Aleutian","US/Arizona", "US/Central","US/East-Indiana","US/Eastern","US/Hawaii","US/Indiana-Starke","US/Michigan", "US/Mountain","US/Pacific","US/Samoa" ] var aTimezones = [], aTimezonesLowerCase = [], aTimezonesSupported = [], aTimezonesSupportedLowerCase = [], oLists = {}, aLegend = [], aLocales = [], aLegendExpanded = [], aLocalesExpanded = [], isSupported = false, isSupportedValues = false, localesHashAll = "", // to compare min to localesHashAllExpanded = "", // to compare min to strMinWarning = " don't panic, it's working<br><br> ... running 'expanded' takes a few seconds", strMaxWarning = " don't panic, it's working<br><br> ... running 'ALL' can take over a minute" function log_console(name) { let hash = mini(sDetail[name]) if (name == "timezones") { console.log(name +"\n", sDetail.timezones) } else if (name == "timezonesall") { console.log("supported timezones\n", Intl.supportedValuesOf('timeZone')) } else if (name == "locales") { console.log(name +": " + hash +"\n", sDetail["locales"]) } else if (sDetail[name] !== undefined) { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once let optExpanded = dom.optExpanded.checked let method = optExpanded ? "expanded" : "list" let list = oLists[method] let proceed = false if (method == "list") { if (aLegend.length == 0) {proceed = true} } else { if (aLegendExpanded.length == 0) {proceed = true} } if (proceed) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.DateTimeFormat.supportedLocalesOf([code]).length > 0 } if (go) { if (method == "list") { aLocales.push(code) } else { aLocalesExpanded.push(code) } let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } if (method == "list") { aLegend.push(code.padStart(7) +": "+ name) } else { aLegendExpanded.push(code.padStart(7) +": "+ name) } } } } // output let aDisplay = (method == "list" ? aLegend : aLegendExpanded) let header = s4 +"LEGEND ["+ aDisplay.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aDisplay.join("<br>") } function run_yr(startyear, addyears = 10) { // add a year to our test to see if increases max unique results // no change by adding all months for: 1800-2026 dom.optExpanded.checked = true setBtn('all') dom.results.innerHTML = '' dom.perf.innerHTML = '' legend() // set legend to expanded let aYears = [] for (let i = 0; i < addyears; i++) { aYears.push(startyear + i) } console.log(aYears) let isSummary = aYears.length > 1 setTimeout(function() { aYears.forEach(function(yr){ run_main('all_confirmed', yr, isSummary) }) }, 50) } function run_main(method, yr, isSummary = false) { if (undefined == method) {return} if (undefined == yr) {isSummary = false} let spacer = "<br><br>" if (method == "all") { // show a warning and add a button with isWarning = true let confirmStr = "are you sure?"+ spacer +"it can take <span class='bad'>up to a minute</span> (or more, on slower devices)" + spacer +"it might <span class='bad'>make your fans whir</span>" + spacer +"<br>" +"<span class='btnbad btnfirst' onClick='run(`all_confirmed`)'>" +"[ YES, i'm sure. RUN it ]</span>" dom.results.innerHTML = confirmStr return } if (method == "all_confirmed") {method = "all"} let t0 = performance.now() let oData = {}, oTempData = {} let optExpanded = dom.optExpanded.checked // Date.UTC(year, monthIndex, day, hour, minute, second, millisecond) let tzNames = ["short","long","shortOffset","longOffset","shortGeneric","longGeneric"] // full list let tzDays = [ new Date('2019-08-15T00:00:00.000Z'), // 2019 August is a sweet spot: nightly 152 = 340 max (154a6a7c) ] if ('all' == method && undefined !== yr) { tzDays.push( new Date(yr +'-01-15T00:00:00.000Z'), new Date(yr +'-02-15T00:00:00.000Z'), new Date(yr +'-03-15T00:00:00.000Z'), new Date(yr +'-04-15T00:00:00.000Z'), new Date(yr +'-05-15T00:00:00.000Z'), new Date(yr +'-06-15T00:00:00.000Z'), new Date(yr +'-07-15T00:00:00.000Z'), new Date(yr +'-08-15T00:00:00.000Z'), new Date(yr +'-09-15T00:00:00.000Z'), new Date(yr +'-10-15T00:00:00.000Z'), new Date(yr +'-11-15T00:00:00.000Z'), new Date(yr +'-12-15T00:00:00.000Z'), ) } tzNames = ["short","shortGeneric","longGeneric"] // reduce test size let tzSG = {"shortGeneric": tzDays}, tzLG = {"longGeneric": tzDays}, tzS = {"short": tzDays}, tzSSG = {"short": tzDays, "shortGeneric": tzDays} let both = {"shortGeneric": tzDays, "longGeneric": tzDays} let everything = { "short": tzDays, "shortGeneric": tzDays, "longGeneric": tzDays, /* these don't add anything "long": tzDays, "shortOffset": tzDays, "longOffset": tzDays, //*/ } let tests = {} if (method == "all") { // all supported timezones minus some crap x both aTimezones.forEach(function(tz) { tests[tz] = everything }) } else if (method == "min") { if (optExpanded) { // expanded tests = { 'Africa/Brazzaville': tzSG, "Africa/Dakar": tzLG, "Africa/Douala": tzLG, "Africa/Lusaka": tzSG, "Africa/Nairobi": tzSG, "America/Argentina/Cordoba": tzSG, "America/Bogota": tzSG, "America/Caracas": tzSG, "America/Cayenne": tzSG, "America/Guayaquil": tzSG, "America/Guyana": tzSG, "America/La_Paz": tzSG, "America/Lima": tzSG, 'America/Mendoza': tzSG, // for blink "America/Montevideo": tzSG, "America/Paramaribo": tzSG, "America/Toronto": tzLG, "America/Winnipeg": tzLG, "Asia/Dili": tzSG, "Asia/Hong_Kong": tzSG, "Asia/Kolkata": tzSG, "Asia/Kuching": tzSG, "Asia/Muscat": tzSG, "Asia/Nicosia": tzLG, 'Asia/Rangoon': tzSG, // for blink "Asia/Seoul": tzLG, "Asia/Shanghai": tzSG, "Asia/Singapore": tzSG, "Asia/Yangon": tzSG, "Asia/Yerevan": tzSG, "Atlantic/Azores": tzSG, "Atlantic/Bermuda": both, "Atlantic/South_Georgia": tzSG, "Australia/Lord_Howe": tzSG, "Europe/Isle_of_Man": tzSG, "Europe/London": tzSSG, "Europe/Minsk": tzSG, // for blink "Europe/Sarajevo": both, "Indian/Cocos": tzS, "Pacific/Pago_Pago": tzLG, "Pacific/Saipan": tzSG, } // test let aMore = [] aMore.forEach(function(tz) { if (undefined == tests[tz]) {tests[tz] = both} }) } else { tests = { "Africa/Dakar": tzLG, "Africa/Douala": tzLG, "Africa/Lusaka": tzSG, "Africa/Nairobi": tzSG, "America/La_Paz": tzSG, "Asia/Seoul": tzLG, "Asia/Shanghai": tzSG, "Asia/Yerevan": tzSG, "Europe/Isle_of_Man": tzSG, "Europe/London": tzSG, "Pacific/Pago_Pago": tzLG, "Pacific/Saipan": tzSG, } } } else if (method == "tiny") { // wee fast test with bang for buck tests = { // gecko non-expanded 226 | expanded 280 "Africa/Douala": tzLG, // +4 "America/Montevideo": tzSG, // +4 "America/Winnipeg": tzLG, // +3 'Asia/Hong_Kong': tzSG, // +4 "Asia/Seoul": tzLG, // +3 "Europe/London": tzSG, // +4 "Asia/Muscat": tzSG, // +4 } } else { // test valid timezone try { let testDate = new Date() let test = new Intl.DateTimeFormat('en', {timeZone: method}).format(testDate) } catch(e) { console.log(e) dom.results.innerHTML = s16 + e.name +": "+ sc + e.message return } /* // only allow supported let methodtest = method.toLowerCase() if (!aTimezonesSupportedLowerCase.includes(methodtest)) { dom.results.innerHTML = s3 + method +": "+ sc +'not supported' return } //*/ tests = {} tests[method] = both } if ('all' == method || 'min' == method || 'tiny' == method) { // remove unsupported: e.g. from tiny/min lists for (const k of Object.keys(tests)) { if (!aTimezonesLowerCase.includes(k.toLowerCase())) {delete tests[k]} } } let tzUsed = [] Object.keys(tests).forEach(function(tz) { tzUsed.push(tz) }) tzUsed.sort() sDetail["timezones"] = tzUsed let oStyles = {} let array = optExpanded ? aLocalesExpanded : aLocales try { array.forEach(function(code) { // for each locale oStyles = {} Object.keys(tests).sort().forEach(function(tz){ // for each tz oStyles[tz] = {} Object.keys(tests[tz]).forEach(function(tzn){ // for each tzname oStyles[tz][tzn] = [] try { // note: use hour12 - https://bugzilla.mozilla.org/show_bug.cgi?id=1645115#c9 // IDK if this is really needed here but it can't hurt // we use y+m+d numeric so we toLocaleString will match // ^ does not affect entropy or hashes at all which are stable across timezones: e.g. TB at UTC vs my machine let option = {year: "numeric", month: "numeric", day: "numeric", hour12: true, timeZone: tz, timeZoneName: tzn} let formatter = Intl.DateTimeFormat(code, option) tests[tz][tzn].forEach(function(d){ // for each date oStyles[tz][tzn].push(formatter.format(d)) /* what do we get if we only use the name let tzDateString = formatter.formatToParts(d).map(({type, value}) => { if (type == "timeZoneName" || type == "unknown") { oStyles[tz][tzn].push(value) } }) //*/ }) } catch (e) { //console.log(e.name, e.message) } // ignore invalid }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oStyles).forEach(function(tz){ // each tz oTempData[hash][tz] = {} Object.keys(oStyles[tz]).forEach(function(tzn){ // for each tzname if (oStyles[tz][tzn].length) { oTempData[hash][tz][tzn] = oStyles[tz][tzn].join(" | ") } }) }) } else { oTempData[hash]["locales"].push(code) } }) //console.log(oTempData) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const tz of Object.keys(oTempData[h]).sort()) { // for each tz if (tz == "locales") { oData[h][tz] = oTempData[h][tz].join(", ") } else { if (Object.keys(oTempData[h][tz]).length) { oData[h][tz] = oTempData[h][tz] } } } } //console.log(oData) let localeGroups = [], displaylist = [] for (const h of Object.keys(oData)) { // for each hash localeGroups.push(oData[h]["locales"]) let localeCount = oData[h]["locales"].split(",").length if (!isSummary) { let str = "" for (const not of Object.keys(oData[h])) { // for each notation if (not !== "locales") { let linecount = Object.keys(oData[h][not]).length if (linecount == 1) { str += "<li>"+ s12 + not +": "+ sc } else { str += "<li>"+ s12 + not + sc +"</li>" } Object.keys(oData[h][not]).forEach(function(s){ // for each style str += s16 + s +": "+ sc + oData[h][not][s] +"</br>" }) if (linecount == 1) {str += "</li>"} } } // wrap into details for long lists if (Object.keys(tests).length > 15) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + h + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[h]["locales"] +"</li></ul>" ) } } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) let tzBtn = "<span class='btn4 btnc' onClick='log_console(`timezones`)'>[" + tzUsed.length + "]</span>" +" from <span class='btn4 btnc' onClick='log_console(`timezonesall`)'>[" + aTimezonesSupported.length + "]</span>" /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { if (optExpanded) { localesHashAllExpanded = localesHash } else { localesHashAll = localesHash } // notate new if expanded and 140+ if (isVer > 139 && optExpanded) { // expanded results if (resultsHash == '0db200af') { // FF151+ } else if (resultsHash == 'b8d2903c') { // FF149-150: 2009907 } else if (resultsHash == '2ca8e181') { // FF147-148 } else if (resultsHash == '31a3ce60') { // FF140-146 } else {resultsHash += ' '+ zNEW } // expanded locales if (localesHash == '154a6a7c') { // FF151+: 340 } else if (localesHash == 'ab48ed50') { // FF147-150: 339 } else if (localesHash == 'dc340cf5') { // FF140-146: 336 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { // don't notate if user hasn't run all if (optExpanded) { if (localesHashAllExpanded !== "") { localesMatch = localesHash == localesHashAllExpanded ? green_tick : red_cross } } else { if (localesHashAll !== "") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } } } // display if (!isSummary) { let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + array.length +"]" + sc //+" &nbsp KEY: <span class='s16'>L</span> longGeneric <span class='s16'>S</span> shortGeneric" + spacer + s12 +"timezones: "+ sc + tzBtn + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") } else { console.log(yr, localesHash, localeGroups.length, tzDays.length) dom.results.innerHTML = dom.results.innerHTML +'<br>' + s4 + yr + sc +': '+ localesHash +' ['+ localeGroups.length +' | '+ tzDays.length + ']' } // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported && isSupportedValues) { try { dom.timezone.innerHTML = Intl.DateTimeFormat().resolvedOptions().timeZone } catch(e) {dom.timezone.innerHTML = ''} let optExpanded = dom.optExpanded.checked // set legend legend() //reset setBtn(method) dom.perf = '' let status = 'calculating ...' if (method == 'all_confirmed') { status += strMaxWarning } else if (method == 'min' && optExpanded) { status += strMinWarning } dom.results.innerHTML = status // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { if (method == "all_confirmed") {method = "all"} // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn try { let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } catch(e) {} } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() dom.optExpanded.checked = false try { // FF91+: pointless if we can't use the feature being tested let formatter = Intl.DateTimeFormat("en-US", {hour12: true, timeZoneName: "longGeneric"}) isSupported = true // FF93+: to simplify matters and help perf, we also require supportedValuesOf let test = Intl.supportedValuesOf("timeZone") isSupportedValues = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // set lists let list = gLocales list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ["en-au,english (australia)","en-bm,english (bermuda)"] //list = ["en,english"] //list = ['de','en-NZ'] list.sort() oLists["list"] = list // expanded: add additional locales to core locales for this test let listExtra = [ "ar-ae,arabic (united arabic emirates)", "ar-bh,arabic (bahrain)", "ar-eg,arabic (egypt)", "ar-ly,arabic (libya)", "ar-sa,arabic (saudi arabia)", "ar-tn,arabic (tunisia)", "az-cyrl,azerbaijani (cyrillic)", "bs-cyrl,bosnian (cyrillic)", "ckb-ir,central kurdish (iran)", "de-ch,german (switzerland)", "en-ae,english (united arab emirates)", "en-ag,english (antigua & barbuda)", "en-at,english (austria)", "en-au,english (australia)", "en-be,english (belgium)", "en-bm,english (bermuda)", "en-bw,english (botswana)", "en-bz,english (belize)", "en-ca,english (canada)", "en-ch,english (switzerland)", "en-gb,english (united kingdom)", "en-gu,english (guam)", "en-gy,english (guyana)", "en-hk,english (hong kong)", "en-ie,english (ireland)", "en-in,english (india)", "en-jm,english (jamaica)", "en-mh,english (marshall islands)", "en-mo,english (macau)", "en-my,english (malaysia)", "en-nz,english (new zealand)", "en-pr,english (puerto rico)", "en-se,english (sweden)", "en-sg,english (singapore)", "en-za,english (south africa)", "en-zw,english (zimbabwe)", "es-ar,spanish (argentina)", "es-bo,spanish (bolivia)", "es-br,spanish (brazil)", "es-bz,spanish (belize)", "es-cl,spanish (chile)", "es-co,spanish (colombia)", "es-cr,spanish (costa rica)", "es-do,spanish (dominican republic)", "es-ec,spanish (ecuador)", "es-mx,spanish (mexico)", "es-pa,spanish (panama)", "es-pe,spanish (peru)", "es-pr,spanish (puerto rico)", "es-us,spanish (united states)", "es-uy,spanish (uruguay)", "es-ve,spanish (venezuela)", "fa-af,persian (afghanistan)", "ff-adlm,fulah (adlam)", "fr-be,french (belgium)", "fr-ca,french (canada)", "fr-ch,french (switzerland)", "fr-gf,french (french guiana)", "fr-gp,french (guadeloupe)", "fr-tn,french (tunisia)", "hi-latn,hindi (latin)", 'kk-cn,kazakh (china)', "ko-kp,korean (north korea)", "kok-latn,konkani (latin)", "ks-deva,kashmiri (devanagari)", "kxv-telu,kuvi (telugu)", "lrc-iq,northern luri (iraq)", "ms-id,malay (indonesia)", "ne-in,nepali (india)", "nl-aw,dutch (aruba)", "nl-be,dutch (belgium)", "nl-sr,dutch (suriname)", "pa-pk,punjabi (pakistan)", "ps-pk,pashto (pakistan)", "pt-ao,portuguese (angola)", "pt-ch,portuguese (switzerland)", "pt-cv,portuguese (cape verde)", "qu-bo,quechua (bolivia)", "qu-ec,quechua (ecuador)", "ro-md,romanian (moldova)", "ru-ua,russian (ukraine)", "sd-deva,sindhi (devanagari)", "se-fi,northern sami (finland)", "shi-latn,tachelhit (latin)", "sr-ba,serbian (bosnia & herzegovina)", "sr-cyrl-me,serbian (cyrillic montenegro)", "sr-latn,serbian (latin)", "sr-latn-ba,serbian (latin bosnia & herzegovina)", "sr-latn-xk,serbian (latin kosovo)", "sr-me,serbian (montenegro)", "sr-xk,serbian (kosovo)", 'sv-fi,swedish (finland)', "sw-cd,swahili (congo kinshasa)", "sw-ke,swahili (kenya)", "ta-my,tamil (malaysia)", "ur-in,urdu (india)", "uz-af,uzbek (afghanistan)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "vai-latn,vai (latin)", "yo-bj,yoruba (benin)", "yue-hans,cantonese (simplified)", "zh-hans-hk,chinese (simplified hong kong)", "zh-hans-mo,chinese (simplified macau)", // blink 'ee-tg,éwé (togo)', 'uz-arab,uzbek (arabic)', ] let expanded = listExtra.concat(list) expanded = expanded.filter(function(item, position) {return expanded.indexOf(item) === position}) // testing //expanded =['nso,northern sotho','tn,tswana','lmo,lombard','kl,greenlandic',] expanded.sort() oLists['expanded'] = expanded legend() if (isSupportedValues) { // FF93+: supportedValuesOf aTimezonesSupported = Intl.supportedValuesOf('timeZone') aTimezonesSupported.sort() aTimezonesSupported.forEach(function(tz) { if (!tzIgnore.includes(tz)) { aTimezones.push(tz) aTimezonesLowerCase.push(tz.toLowerCase()) } aTimezonesSupportedLowerCase.push(tz.toLowerCase()) }) } if (isSupported && isSupportedValues) { try {dom.timezone.innerHTML = Intl.DateTimeFormat().resolvedOptions().timeZone} catch(e) {} setBtn('min') dom.results = 'calculating ...' setTimeout(function() { run_main('min') }, 50) } }) </script> </body> </html> ================================================ FILE: tests/duration.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>durationformat</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2"> <div class="nav-title">durationformat <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy in Intl.DurationFormat</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], oTestData = {}, isSupported = false, localesHashAll = "" // to compare min to // when numberingSystem is undefined, what does it use exactly? // undefined = 216 // all = 216 // anyway, looks like we don't care about per numbering system for entropy let aNumsys = [ 'adlm', // 210 'ahom', // 210 'arab', // 187 'arabext', // 188 'bali', // 210 'beng', // 210 'bhks', // 210 'brah', // 210 'cakm', // 210 'cham', // 210 'deva', // 210 'diak', // 210 'fullwide', // 209 'gara', // 210 'gong', // 210 'gonm', // 210 'gujr', // 210 'gukh', // 210 'guru', // 210 'hanidec', // 210 'hmng', // 210 'hmnp', // 210 'java', // 210 'kali', // 210 'kawi', // 210 'khmr', // 210 'knda', // 210 'krai', // 210 'lana', // 210 'lanatham', // 210 'laoo', // 210 'latn', // 210 'lepc', // 210 'limb', // 210 'mathbold', // 210 'mathdbl', // 210 'mathmono', // 210 'mathsanb', // 210 'mathsans', // 210 'mlym', // 210 'modi', // 210 'mong', // 210 'mroo', // 210 'mtei', // 210 'mymr', // 210 'mymrepka', // 210 'mymrpao', // 210 'mymrshan', // 210 'mymrtlng', // 210 'nagm', // 210 'newa', // 210 'nkoo', // 210 'olck', // 210 'onao', // 210 'orya', // 210 'osma', // 210 'outlined', // 210 'rohg', // 210 'saur', // 210 'segment', // 210 'shrd', // 210 'sind', // 210 'sinh', // 210 'sora', // 210 'sund', // 210 'sunu', // 210 'takr', // 210 'talu', // 210 'tamldec', // 210 'telu', // 210 'thai', // 210 'tibt', // 210 'tirh', // 210 'tnsa', // 210 'undefined', // 216 'vaii', // 210 'wara', // 210 'wcho', // 210 ] function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = Intl.DurationFormat.supportedLocalesOf([code]).length > 0 if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function testitems() { //loop each numsys on it's own dom.perf = "" dom.results = "" oTestData = {} setTimeout(function() { try { aNumsys.forEach(function(item){ // allow testing arrays of scripts let testarray = 'object' == typeof item ? item : [item] run_main('all', testarray, true) }) } catch(e) { console.log(e) } }, 5) } function run_main(method, aTest, isLoop = false) { if (!isSupported) {return} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DurationFormat/DurationFormat let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" if (!isLoop) { dom.perf = "" dom.results = "" } let styles = ['digital','long','narrow','short'] let options = { '0000': {'years': 0, 'months': 0,'weeks': 0,'days': 0,'hours': 0,'minutes': 0,'seconds': 0,'milliseconds': 0,'microseconds': 0,'nanoseconds': 0}, '0001': {'years': 1, 'months': 1,'weeks': 1,'days': 1,'hours': 1,'minutes': 1,'seconds': 1,'milliseconds': 1,'microseconds': 1,'nanoseconds': 1}, '0002': {'years': 2, 'months': 2,'weeks': 2,'days': 2,'hours': 2,'minutes': 2,'seconds': 2,'milliseconds': 2,'microseconds': 2,'nanoseconds': 2}, '1k': {'years': 1000, 'months': 1000,'weeks': 1000,'hours': 1000,'minutes': 1000,'seconds': 1000,'milliseconds': 1000,'microseconds': 1000,'nanoseconds': 1000}, //'10k': {'years': 10000, 'months': 10000,'weeks': 10000,'hours': 10000,'minutes': 10000,'seconds': 10000,'milliseconds': 10000,'microseconds': 10000,'nanoseconds': 10000}, 'mixed': {'years': 0, 'months': 1,'weeks': 2,'days': 3,'hours': 4,'minutes': 5,'seconds': 6,'milliseconds': 7,'microseconds': 8,'nanoseconds': 9}, 'split': {'years': 1, 'nanoseconds': 1}, // there's also how things are joined e.g. "0 y e 1 ns" (zero years and 1 nanosecond) } let digitaloptions = { '0000': {'hours': 0,'minutes': 0,'seconds': 0,'milliseconds': 0,'microseconds': 0,'nanoseconds': 0}, '0001': {'hours': 1,'minutes': 1,'seconds': 1,'milliseconds': 1,'microseconds': 1,'nanoseconds': 1}, '0002': {'hours': 2,'minutes': 2,'seconds': 2,'milliseconds': 2,'microseconds': 2,'nanoseconds': 2}, } let tests = {} if ('all' == method) { styles.forEach(function(s) { if ('digital' == s) {tests[s] = digitaloptions} else {tests[s] = options} }) //console.log(tests) } else if (method == "min") { // all styles add entropy: digital: 13 | long: 4 | narrow: 5 | short: 2 tests = { 'digital': {'a': {'milliseconds': 1}}, 'long': {'a': {'years': 1, 'microseconds': 1}, 'b': {'seconds': 2}}, 'narrow': {'a': {'years': 1, 'months': 2, 'seconds': 1, 'microseconds': 1000}}, 'short': {'a': {'days': 2, 'seconds': 2, 'nanoseconds': 1}}, } } let numsys = [undefined] numsys = undefined == aTest ? numsys : aTest numsys.sort() /* // which split makes the difference if ('all' == method) { //delete tests.long.split delete tests.narrow.split // narrow is useless delete tests.short.split // short is useless console.log(tests) } //*/ //console.log(tests) try { aLocales.forEach(function(code) { let oStyles = {} Object.keys(tests).forEach(function(s){ // each style oStyles[s] = {} numsys.forEach(function(ns){ // each numberingsystem oStyles[s][ns] = {} // set formatter once per code + style // always display so we catch 0's let nsvalue = ns // make sure undefined is nsvalue and "undefined" is ns (our key) if ('undefined' == ns || undefined == ns) { nsvalue = undefined ns = 'undefined' } let formatoptions, splitoptions if ('digital' == s) { formatoptions = { 'style': s, 'numberingSystem': nsvalue, } } else { splitoptions = { 'style': s, 'numberingSystem': nsvalue, } formatoptions = { 'style': s, 'numberingSystem': nsvalue, 'yearsDisplay': 'always', 'monthsDisplay': 'always', 'weeksDisplay': 'always', 'daysDisplay': 'always', 'hoursDisplay': 'always', 'minutesDisplay': 'always', 'secondsDisplay': 'always', 'millisecondsDisplay': 'always', 'microsecondsDisplay':'always', 'nanosecondsDisplay': 'always', } if ('min' == method) { // we only seem to need one lot of zero formatoptions = { 'style': s, 'numberingSystem': nsvalue, 'yearsDisplay': 'always', } } } let formatter = new Intl.DurationFormat(code, formatoptions) let splitformatter = new Intl.DurationFormat(code, splitoptions) // for each test Object.keys(tests[s]).sort().forEach(function(t){ if ('split' == t) { oStyles[s][ns][t] = splitformatter.format(tests[s][t]) } else { oStyles[s][ns][t] = formatter.format(tests[s][t]) } }) }) }) let hash = mini(oStyles) +" " // make numbers sort like strings //if (code == 'yo-bj') {console.log(code, hash, oStyles)} if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] numsys.forEach(function(ns){ // each numberingsystem oTempData[hash][ns] = {} Object.keys(tests).sort().forEach(function(s){ oTempData[hash][ns][s] = {} Object.keys(tests[s]).sort().forEach(function(t){ oTempData[hash][ns][s][t] = oStyles[s][ns][t] }) }) }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == "locales") { oData[n][p] = oTempData[n][p].join(", ") } else { oData[n][p] = oTempData[n][p] } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length if (!isLoop) { let displayData = [] for (const ns of Object.keys(oData[k])) { if ('locales' !== ns) { displayData.push(s14 + 'numberingSystem: '+ns + sc) for (const s of Object.keys(oData[k][ns])) { if ('min' == method) { let sInitial = s.slice(0,1).toUpperCase() let aTests = [] for (const t of Object.keys(oData[k][ns][s])) { aTests.push(oData[k][ns][s][t]) } displayData.push('<li>'+ s16 + sInitial +": "+ sc + aTests.join(' | ') +"</li>") } else { displayData.push('<li>'+ s16 + s +": "+ sc) for (const t of Object.keys(oData[k][ns][s])) { let x = parseInt(t) if (isNaN(x)) {x = t} if ('1k' == t) (x = t) displayData.push('<li>'+ s12 + x +": "+ sc + oData[k][ns][s][t] +"</li>") } displayData.push('</li>') } } } } let strData = displayData.join('') if ('all' == method) {strData = '<details>' + strData +'</details>'} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ strData + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } } if (isLoop) { let testCount = localeGroups.length // record it if (undefined == oTestData[testCount]) {oTestData[testCount] = []} oTestData[testCount].push(numsys) //console.log(testCount, aUsed) /* dom.results.innerHTML = dom.results.innerHTML + '<br>' + s16 + testCount + sc +': '+ '[\''+ aUsed.join('\', \'') +'\']' //*/ dom.results.innerHTML = dom.results.innerHTML + '<br>' + '\''+ numsys.join('\', \'') +'\', // ' + testCount return } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == '92b05a5c') { // FF151+ } else if (resultsHash == '319ff3fd') { // FF147-150 } else if (resultsHash == '1294206a') { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == '97048ba7') { // FF151+: 219 } else if (localesHash == '91d4e56b') { // FF147-150: 218 } else if (localesHash == 'c5d9dc08') { // FF140-146: 216 } else if (isFF) {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // add additional locales to core locales for this test let aListExtra = [ "ar-bh,arabic (bahrain)", "ar-dz,arabic (algeria)", "ar-sa,arabic (saudi arabia)", "bs-cyrl,bosnian (cyrillic)", "de-at,german (austria)", "de-ch,german (switzerland)", "en-at,english (austria)", 'en-au,english (australia)', "en-ca,english (canada)", "en-ch,english (switzerland)", "en-dk,english (denmark)", "en-fi,english (finland)", "en-se,english (sweden)", "es-419,spanish (latin america and the caribbean)", "es-ar,spanish (argentina)", "es-bo,spanish (bolivia)", "es-co,spanish (colombia)", "es-cr,spanish (costa rica)", "es-do,spanish (dominican republic)", "es-mx,spanish (mexico)", "es-py,spanish (paraguay)", "es-us,spanish (united states)", "ff-adlm,fulah (adlam)", "fr-ca,french (canada)", 'fr-ch,french (switzerland)', "fr-lu,french (luxembourg)", "hi-latn,hindi (latin)", "it-ch,italian (switzerland)", 'kk-cn,kazakh (china)', "kok-latn,konkani (latin)", "ks-deva,kashmiri (devanagari)", "kxv-telu,kuvi (telugu)", "ms-bn,malay (brunei)", "ms-id,malay (indonesia)", "ps-pk,pashto (pakistan)", "pt-ao,portuguese (angola)", "pt-ch,portuguese (switzerland)", "pt-pt,portuguese (portugal)", "qu-bo,quechua (bolivia)", "ro-md,romanian (moldova)", "ru-ua,russian (ukraine)", "sr-ba,serbian (bosnia & herzegovina)", "sr-cyrl-me,serbian (cyrillic montenegro)", "sr-latn,serbian (latin)", "sr-latn-ba,serbian (latin bosnia & herzegovina)", "sv-fi,swedish (finland)", "sw-cd,swahili (congo kinshasa)", "sw-ke,swahili (kenya)", "ur-in,urdu (india)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "yo-bj,yoruba (benin)", "yue-cn,cantonese (china)", //* blink 'en-001,english', 'az-cyrl,azerbaijani (cyrillic)', 'pa-arab,punjabi (arabic)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en,english','fr,french','de,german'] //list = ['en,english'] //list = ['wo,wolof','tg,tajik','ie,interlingue','ln,lingala'] // example of split durations adding 'and" //list = ['af','en','fr','he'] list.sort() legend() // check supported try { let test = new Intl.DurationFormat('en').resolvedOptions() isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // check bigint support: FF68+ try { let y = BigInt("9999999999999999") isBigIntSupported = true } catch(e) {} setBtn("all") setTimeout(function() { run_main("all") }, 100) }) </script> </body> </html> ================================================ FILE: tests/elementfont.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>elements: fonts</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 480px; max-width: 780px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#elements">return to TZP index</a></td></tr> </table> <table id="tb15"> <col width="15%"><col width="85%"> <thead><tr><th colspan="2"> <div class="nav-title">elements: fonts <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="perf" id="locale"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">basic font element test, using clientrect, with different variables</span> <br><br> <span class="btnfirst btn15" onClick="run()">[ run ]</span> <input type="checkbox" name="expand" style="margin: 0; height: 12px" onClick='run()'> <span class="no_color">expand font-styles</span> &nbsp; <b>|</b> &nbsp; <span class="btn btn15" onClick="run_lang()">[ scripts ]</span> </td></tr> <tr> <td colspan="2" style="text-align: left;"> <hr><br> <span class="no_color c mono spaces" id="results"></span></td> </tr> </table> <br> <script> 'use strict'; let isLanguage = '', isLocale = '' let sizeA = ['3.9pt','141.7pt','266.6pt',] let sizeB = ['3.9pt','xx-small','x-small','small','medium','large','x-large','xx-large','xxx-large','141.7pt','266.6pt'] let oList = { // keep in sorted order // https://developer.mozilla.org/en-US/docs/Web/CSS/generic-family 'cursive': sizeA, 'emoji': sizeA, // windows: emoji = serif 'fangsong': sizeA, 'fantasy': sizeA, // windows: fantasy = sans 'math': sizeA, 'monospace': sizeB, 'sans-serif': sizeB, 'serif': sizeB, 'system-ui': sizeA, } let aExpand = [ 'none', 'ui-monospace', "ui-sans-serif", 'ui-serif', "ui-rounded" ] let bulkData = {} function get_element_font(lang = '') { let t0 = performance.now() let oUsed = {} for (const k of Object.keys(oList)) {oUsed[k] = oList[k]} if (dom.expand.checked) {aExpand.forEach(function(k){oUsed[k] = sizeB})} const id = 'element-fp' let hash, data = {}, method try { const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) let oData = {}, tmpobj = {} for (const k of Object.keys(oUsed).sort()) { let sizes = oUsed[k] let tmpsizes = [] sizes.forEach(function(size) { // create + measure each individually as preceeding elements can affect subsequent ones dom[id].innerHTML = "<div lang='"+ lang +"' style='font-size:"+ size +";' class='"+ k +"'>...</div>" let target = dom[id].firstChild // method method = target.getBoundingClientRect() // width+height = max entropy tmpsizes.push([size, method.width, method.height, method.x, method.y]) }) let sizehash = mini(tmpsizes) if (oData[sizehash] == undefined) {oData[sizehash] = {data: tmpsizes, group: [k]} } else {oData[sizehash].group.push(k)} } // group by styles for (const k of Object.keys(oData)){data[oData[k].group.join(' ')] = oData[k].data} let count = Object.keys(data).length hash = mini(data) dom.results.innerHTML = s15 + hash + sc +'<br>'+ json_highlight(data, 105) } catch(e) { dom.results.innerHTML = e+'' } // remove element removeElementFn(id) dom.perf.innerHTML = Math.round(performance.now() -t0) +" ms" } try {isLocale = Intl.DateTimeFormat().resolvedOptions().locale} catch(e) {isLocale = zErr} try {isLanguage = navigator.language} catch(e) {isLanguage = zErr} try {dom.locale.innerHTML = isLanguage +' | '+ isLocale} catch(e) {console.log(e)} function run() { dom.results = "" dom.perf = "" setTimeout(function() { get_element_font() }, 170) } dom.expand.checked = false get_element_font() //make it easy to check all zoom values if (isFile) { //window.addEventListener("resize", get_element_font) } function run_lang() { let t0 = performance.now() bulkData = {} let oUsed = {} // unique measurement sets per style: this is all we need: = same on windows FF as sizeB // windows TB almost the same: 'x-large' = +1 sans-serif | 'xxx-large' = +1 monospace // the bigger the better it seems // perf is atrocious at 100ms let sizes = ['3.6pt','947.7pt'] // one small helps, and one large but not too large seems sufficient (at least on windows) // 947.7pt is better than 862.3 - but 10k+ is a disaster and 1137.7pt is slightly worse than 947.7 // it's just going to vary slightly with DPR and locale/language and platform/fonts // the above is good: 1 small, 1 large for (const k of Object.keys(oList)) {oUsed[k] = oList[k]} // one of each script (from default script sizes) let aList = [ 'bn','bo','en','gu','he','hi','ja','ka','km','kn','ko','ml','or','pa','si','ta','te','th','x-math','zh-CN','zh-TW', // these make no diff on win FF, but some do on win TB, etc, so always include all 29 scripts 'ar','cr','el','gez','hy','my','ru','zh-HK', // blank seems to be useless, they don't indicate anything such as locale/applang AFAICT // TB: this always matches en: is this because it's a en-US build '', ] aList.sort() /* WINDOWS FF: (sizeB) fa405cbc { "cursive": 6, "emoji": 20, "fangsong": 2, "fantasy": 2, "monospace": 18, "sans-serif": 15, "serif": 21, "system-ui": 4 } WINDOWS TB: 075fbad5 { "cursive": 2, "emoji": 21, "fangsong": 21, "fantasy": 2, "monospace": 17, "sans-serif": 17, "serif": 19, "system-ui": 21 } so: at this point I think we get enough entropy just by adding all scripts to FF serif and */ // ToDo: group data by sizes and by script (we already have it by style) const id = 'element-fp' try { const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) // populate our object keys for (const style of Object.keys(oUsed).sort()) {bulkData[style] = {}} //group by style then hash of the sizes let method aList.forEach(function(lang) { for (const style of Object.keys(oUsed).sort()) { let tmpsizes = [] sizes.forEach(function(size) { // create + measure each individually as preceeding elements can affect subsequent ones if ('' == lang) { dom[id].innerHTML = "<div style='font-size:"+ size +";' class='"+ style +"'>...</div>" } else { dom[id].innerHTML = "<div lang='"+ lang +"' style='font-size:"+ size +";' class='"+ style +"'>...</div>" } let target = dom[id].firstChild // method method = target.getBoundingClientRect() // width+height = max entropy tmpsizes.push([size, method.width, method.height, method.x, method.y]) }) let hash = mini(tmpsizes) if (undefined == bulkData[style][hash]) { bulkData[style][hash] = {'data': tmpsizes, 'group': [lang]} } else { bulkData[style][hash].group.push(lang) } if ('en' == lang) { bulkData[style][hash]['en'] = true } else if ('' == lang) { bulkData[style][hash]['blank'] = true } } }) } catch(e) { dom.results.innerHTML = e+'' } let perf = performance.now() - t0 // remove element removeElementFn(id) console.log(perf, 'ms |', mini(bulkData), '|', aList.length) let oCounts = {} for (const k of Object.keys(bulkData)) {oCounts[k] = Object.keys(bulkData[k]).length} console.log(bulkData) dom.results.innerHTML = s15 + mini(oCounts) + sc +'<br>'+ json_highlight(oCounts, 105) + '<br><br>'+ s15 + mini(bulkData) + sc +'<br>'+ json_highlight(bulkData, 105) dom.perf.innerHTML = Math.round(perf) +" ms" } </script> </body> </html> ================================================ FILE: tests/elementforms.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>elements: forms</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <link rel="stylesheet" href="chrome://global/locale/intl.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 780px; max-width: 780px} /* tweak overlay for this PoC */ #overlay { width: 500px; min-width: 300px; } #overlay div.pad {padding: 0px 25px 15px;} .mozInputs > input, .mozInputs > select, .mozInputs > progress { -moz-appearance: none; -webkit-appearance: none; appearance: none; } .revert { all: revert; } /** custom input styles **/ /*details, input, option {font-size: 45px;}*/ </style> </head> <body> <div id="element-fp"></div> <div id="modaloverlay" onClick="hide_overlays()"></div> <div id="overlay"> <div class="mono spaces pad"><span id="overlaytop"></span> <div style="text-align: right;" id="overlaybuttons"><span class='btn0 btnc' onClick='hide_overlays()'>[CLOSE]</span></div> <p>These are examples only. The real test creates, measures, and then removes each element individually, as the order of and/or the presence of other elements can affect results.</p> <br><input type='radio' id="demoNative" onchange="style_demo()" name="demostyle" checked> NATIVE STYLED <input type='radio' onchange="style_demo()" name="demostyle"> UNSTYLED <div class="normalized"> <p class="native"> <input style="display:inline;" type="button"> &nbsp; <input style="display:inline;" type="checkbox"> &nbsp; <input style="display:inline;" type="radio"> &nbsp; <input style="display:inline;" type="color"> &nbsp; <input style="display:inline;" type="reset"> &nbsp; <select style="display:inline;"><option></option></select> &nbsp; </p> <p class="native"><input style="display:inline;" type="date"> &nbsp; <input style="display:inline;" type="time"></p> <p class="native"><input style="display:inline;" type="datetime-local"></p> <!-- week/month for chrome--> <p class="native"><input style="display:inline;" type="month"> &nbsp; <input style="display:inline;" type="week"></p> <p class="native"><input style="display:inline;" type="file" webkitdirectory directory></p> <p class="native"><input style="display:inline;" type="file" ></p> <p class="native"><input style="display:inline;" type="file" multiple=""></p> <p class="native"><input style="display:inline; color: var(--test0);" type="image"></p> <p class="native"><input style="display:inline;" type="number"> &nbsp; <input type="submit" style="display:inline;"></p> <p class="native"> <select style="display:inline;" multiple=""><option></option></select> &nbsp; <select style="display:inline;" multiple=""><option> &nbsp; &nbsp; &nbsp; </option></select> &nbsp; <select style="display:inline;" multiple=""><option>Mōá?-&#xffff;</option></select> </p> <p class="native"><details style="display:inline;"></details></p> <p class="native"><input style="display:inline;" type="range" min="0" max="2" value="1"></p> </div> </div> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#elements">return to TZP index</a></td></tr> </table> <!-- app lang --> <table id="tb15"> <col width="25%"><col width="75%"> <thead><tr><th colspan="2"> <div class="nav-title">elements: forms <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="perf" id="locale"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="btn15 btnfirst" onClick="run()">[ run ]</span> <span class="btn15 btn" onClick="run(true)">[ TZP ]</span> <span class="no_color"> <input id="optVertical" checked type="checkbox"> &nbsp; vertical <input id="optUnstyled" type="checkbox"> &nbsp; unstyled &nbsp; | <input id="optEverything" onChange="style_everything()" type="checkbox"> &nbsp; everything &nbsp; | &nbsp; <a class='blue' href='https://searchfox.org/mozilla-central/source/dom/locales/en-US/chrome/layout/HtmlForm.properties' target='blank'>searchfox HtmlForm</a> <span class="btn btn0" onCLick="show_overlay()">[ example ]</span> </span> </td></tr> <tr><td colspan="2"><hr><br></td></tr> <tr><td colspan="2" style="text-align: left;"> <div class="c mono spaces no_color" id="elements"></div> </td></tr> </table> <br> <script> 'use strict'; let isLocale = "" let isTestSets = false let oData = {} dom.optVertical.checked = true dom.optUnstyled.checked = false dom.optEverything.checked = false dom.demoNative.checked = true function style_demo() { // flip style in examples let addstyle = dom.demoNative.checked ? "native" : "mozInputs" let remstyle = addstyle == "native" ? "mozInputs" : "native" try { let items = document.querySelectorAll("."+ remstyle) for (let i=0; i < items.length; i++) { items[i].classList.add(addstyle) items[i].classList.remove(remstyle) } } catch(e) {} } function style_everything() { let isEverything = dom.optEverything.checked dom.optVertical.disabled = isEverything dom.optUnstyled.disabled = isEverything } style_everything() try { // escape to close document.onkeydown = function(evt) { evt = evt || window.event; var isEscape = false; if ("key" in evt) { isEscape = (evt.key === "Escape" || evt.key === "Esc"); } else { isEscape = (evt.keyCode === 27); } if (isEscape) { hide_overlays() } } overlay.addEventListener("keydown", (e) => { console.log(e.key) }) } catch(e) { console.log(e) } function get_elements(isTZP = false) { let t0 = performance.now() // ui.useOverlayScrollbars: 0=on, 1=off // windows 11 on -> off (en-US) // empty 29 -> 12 = -17 // spaces 49 -> 32 = -17 // string 78 -> 61 = -17 // matches scrollbar in document/viewport/element // I expect this will _have_ to leak subpixels given we are measuring with clientRect let tmpData = {}, target let isEverything = dom.optEverything.checked let aStyles = [], aWritingStyles = [] let params = [] if (isTZP) { aWritingStyles = ['vertical-lr'] aStyles = ['native','unstyled'] params = ['TZP'] } else if (isEverything) { aWritingStyles = ['horizontal-tb','vertical-lr'] aStyles = ['native','unstyled'] params = ['everything'] } else { if (dom.optVertical.checked) {aWritingStyles = ['vertical-lr']} else {aWritingStyles = ['horizontal-tb']} if (dom.optUnstyled.checked) {aStyles = ['unstyled']} else {aStyles = ['native']} params.push(aWritingStyles[0].slice(0,-3)) params.push(aStyles[0]) } let oList = { button: '', checkbox: '', color: '', date: '', "datetime-local": '', file: '', image: '', number: '', radio: '', range: '', reset: '', submit: '', time: '', details: '<details></details>', details_open: '<details open="">.</details>', progress: '<progress></progress>', select: '<select><option></option></select>', select_empty: '<select multiple=""><option></option></select>', select_empty_option: '<select multiple=""><option></option></select>', select_spaces: '<select multiple=""><option> &nbsp; &nbsp; &nbsp; </option>"</select>', select_spaces_option: '<select multiple=""><option> &nbsp; &nbsp; &nbsp; </option>"</select>', select_string: '<select multiple=""><option>Mōá?-&#xffff;</option></select>', select_string_option: '<select multiple=""><option>Mōá?-&#xffff;</option></select>', textarea: '<textarea></textarea>', textarea_3x5: '<textarea cols="5" rows="3"></textarea>', // always == file directory: '<input webkitdirectory directory type="file">', files: '<input multiple="" type="file">', // gecko: should always = number datetime: '', email: '', month: '', password: '', search: '', tel: '', text: '', url: '', week: '', // always 0 hidden: '', } let oTZP = { 'native': { button: '', checkbox: '', color: '', date: '', 'datetime-local': '', details: '<details></details>', 'details_open': '<details open="">.</details>', file: '', image: '', month: '', number: '', progress: '<progress></progress>', radio: '', range: '', reset: '', select: '<select><option></option></select>', select_empty: '<select multiple=""><option></option></select>', select_empty_option: '<select multiple=""><option></option></select>', select_spaces: '<select multiple=""><option> &nbsp; &nbsp; &nbsp; </option>"</select>', select_spaces_option: '<select multiple=""><option> &nbsp; &nbsp; &nbsp; </option>"</select>', select_string: '<select multiple=""><option>Mōá?-&#xffff;</option></select>', select_string_option: '<select multiple=""><option>Mōá?-&#xffff;</option></select>', submit: '', textarea: '<textarea></textarea>', textarea_3x5: '<textarea cols="5" rows="3"></textarea>', time: '', week: '', // month + week are same as number but not in chrome | gecko may follow suit }, 'unstyled': { // differ on windows // ToDo: check linux/mac/android checkbox: '', progress: '<progress></progress>', radio: '', select: '<select><option></option></select>', } } try { const parent = dom['element-fp'] let count = 0 let oSets = { setAll: new Set(), // 2 setWH: new Set(), setWX: new Set(), // terrible setWY: new Set(), // 57/59 setHX: new Set(), // 56/59 setHY: new Set(), setXY: new Set(), // 3 setWHX: new Set(), // 59/59 setWHY: new Set(), // 57 } aStyles.forEach(function(style){ if (isTZP) { tmpData[style] = {} } else if (isEverything) { tmpData[style] = {} } aWritingStyles.forEach(function(writingstyle){ if (isTZP) { } else if (isEverything) { tmpData[style][writingstyle] = {} } let oItems = isTZP ? oTZP[style] : oList for (const k of Object.keys(oItems).sort()) { count++ // important to clear the div so no other elements can affect measurements parent.innerHTML = "" let data = [] try { parent.innerHTML = ('' == oList[k] ? '<input type="'+ k +'">' : oList[k]) target = parent.firstChild target.setAttribute("style","display:inline; writing-mode: "+ writingstyle +";") if (style == "unstyled") {target.classList.add('unstyled')} if (k.includes('_option')) {target = target.lastElementChild} let method = target.getBoundingClientRect() data = [method.width, method.height, method.x, method.y] oSets.setAll.add(data.join(' ')) if (isTestSets) { // 2s oSets.setWH.add(method.width +' '+ method.height) oSets.setWX.add(method.width +' '+ method.x) oSets.setWY.add(method.width +' '+ method.y) oSets.setHX.add(method.height +' '+ method.x) oSets.setHY.add(method.height +' '+ method.y) oSets.setXY.add(method.x +' '+ method.y) // 3s oSets.setWHX.add(method.width +' '+ method.height +' '+ method.x) oSets.setWHY.add(method.width +' '+ method.height +' '+ method.y) } if (isTZP) { let itemhash = mini(data) if (undefined == tmpData[style][itemhash]) {tmpData[style][itemhash] = {'data': data, 'group': [k]} } else {tmpData[style][itemhash]['group'].push(k)} } else if (isEverything) { tmpData[style][writingstyle][k] = data } else { tmpData[k] = data } } catch(e) { if (isTZP) { tmpData[style][k] = [k +", "+ e+''] } else if (isEverything) { tmpData[style][writingstyle][k] = [e+''] } else { tmpData[k] = [e+''] } } } }) }) parent.innerHTML = "" // just in case oData = {} // reset if (isTZP) { // group by results let newobj = {} for (const key of Object.keys(tmpData)) { newobj[key] = {} for (const k of Object.keys(tmpData[key])) { newobj[key][tmpData[key][k].group.join(' ')] = tmpData[key][k]['data'] } } for (const key of Object.keys(newobj)) { oData[key] = {} for (const k of Object.keys(newobj[key]).sort()) {oData[key][k] = newobj[key][k]} } } else { for (const k of Object.keys(tmpData).sort()) { if (isEverything) { oData[k] = {} for (const j of Object.keys(tmpData[k]).sort()) { oData[k][j] = {} for (const m of Object.keys(tmpData[k][j]).sort()) { oData[k][j][m] = tmpData[k][j][m] } } } else { oData[k] = tmpData[k] } } } let hash = mini(oData), notation = "" let t1 = performance.now() try {dom.perf.innerHTML = Math.round(t1 - t0) +" ms"} catch(e) {} let strParam = (params.length ? " ["+ params.join(" ") +"]" : "") let strCount = ' [' + count + ' items | ' + oSets.setAll.size + ' unique]' let display = [], tmphash = '' if (!isTZP && isEverything) { for (const k of Object.keys(oData).sort()) { tmphash = mini(oData[k]) display.push(s15 + k.toUpperCase() +": " + sc + tmphash +"<br>") for (const j of Object.keys(oData[k]).sort()) { tmphash = mini(oData[k][j]) display.push(s15+ (k +" "+ j).toUpperCase() +": " + sc + tmphash +"<br>") display.push(json_highlight(oData[k][j], 120) +"<br>") } } hash = s15 +"EVERYTHING: " + sc + hash } dom.elements.innerHTML = hash + notation + strParam + strCount + "<br>" + (!isTZP && isEverything ? "<br>" + display.join("<br>") : json_highlight(oData, 120)) if (isTestSets) { console.log('---\n' + (isEverything ? 'everything' : params.join(' '))) for (const k of Object.keys(oSets)) { console.log(k, oSets[k].size) } } return } catch(e) { dom.elements = e+"" return } } function run(isTZP = false) { // clear dom.elements.innerHTML = " &nbsp; " // pause so users see change setTimeout(function() { get_elements(isTZP) }, 170) } try {isLocale = Intl.DateTimeFormat().resolvedOptions().locale} catch(e) {isLocale = zErr} try {dom.locale.innerHTML = isLocale} catch(e) {} Promise.all([ get_globals() ]).then(function(){ style_everything() run() }) </script> </body> </html> ================================================ FILE: tests/elementkeys.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>element keys</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 90%; min-width: 480px; max-width: 780px} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#elements">return to TZP index</a></td></tr> </table> <table id="tb15"> <thead><tr><th> <div class="nav-title">element keys <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="c perf" id="perf2"></span></div> </div> </th></tr></thead> <tr><td class="intro"> <span class="no_color"></span> <span class="btn15 btnfirst" onClick="run()">[ run ]</span> <input type="checkbox" id="optAll"> <span class="no_color">all elements </span> </td></tr> <tr><td><hr></td></tr> <tr><td></td></tr> <tr><td style="text-align: left; color: var(--test0);" class="mono spaces" id="details"></td></tr> </table> <br> <script> 'use strict'; //https://w3c.github.io/elements-of-html/ // ignore: checked in gecko + blink (desktop) // all match even unsorted let oListIgnore = { "CHECK": [ // input types = input 'input|button','input|checkbox','input|color','input|date','input|datetime', 'input|datetime-local','input|file','input|image','input|month','input|number', 'input|password','input|radio','input|range','input|reset','input|search', 'input|submit','input|tel','input|text','input|time','input|url','input|week', ], "STABLE B": [ 'caption','h1','h2','h3','h4','h5','h6','p', // = div 'ins', // = del 'xmp', // = pre 'dl','menu', // = dir 'q', // = blockquote 'tfoot','thead', // = tbody 'th', // = td 'colgroup', // = col // = span 'abbr','acronym','address','applet','article','aside','b', 'basefont','bdi','bdo','big','center','cite','code','dd','dfn','dt', 'element-details','em','figcaption','figure','footer','head','header','hgroup','i','isindex', 'kbd','keygen','main','mark','menuitem','nav','nextid','nobr','noembed', 'noframes','noscript','picture','plaintext','portal','rb','rbc','rp','rt', 'rtc','ruby','s','samp','search','section','small','strike','strong','sub', 'summary','sup','tt','u','var','wbr', ], } let oList = { // as of 137n "CHECK A": [ // changes since FF128 // cold 3.5 | rerun 1.7 'audio', // 116 setSinkId, sinkId // 128 (seekToNextFrame) // 137n allowedToPlay 'button', // 125 popoverTargetElement, popoverTargetAction // 144 commandForElement, command 'details', // 130 name 'dialog', // first added in 98 (show, showModal, close, open, returnValue) // 139 requestClose 'template', // 123 shadowRootMode, shadowRootDelegatesFocus // 125 shadowRootClonable // 128 shadowRootSerializable 'video', // 116 setSinkId, sinkId // 122 disablePictureInPicture // 128 (seekToNextFrame) // 132 requestVideoFrameCallback, cancelVideoFrameCallback // 137n allowedToPlay 'img', // 132 fetchPriority //'link', // 132 fetchPriority 'script', // 132 fetchPriority // 135 innerText, textContent 'input', // 116 dirName // 125 popoverTargetElement, popoverTargetAction // 142 (mozIsTextField) ], "STABLE A": [ // no changes since FF126 // fast since the parser is already created // cold: 0.7 | rerun 0.7 'iframe', // 121 (loading) 'marquee', // 126 (onbounce, onfinish, onstart) 'select', // 122 showPicker 'textarea', // 116 dirName ], // no changes since FF111 "STABLE B": [ //* 'source', // 108 (width, height) 'meta', // 106 (media) 'form', // 75, 111 (rel, relList) 'canvas', // 74, 105 'object', // 68 'slot', // assignedNodes + name (63) assignedElements (66), assign (92) 'body', // 57, 69, 89 'frameset', // 57, 69, 89 'meter', // 56 (labels) 'progress', // 56 (labels) 'style', // disabled, media, type, sheet (scoped removed in 55) //* super boring // FF52-130+ 'a', 'area', 'base', // href, target 'blockquote', // cite 'br', // clear 'col', 'data', // value 'datalist', // options 'del', // cite, dateTime 'dir', // compact 'div', // align 'embed', 'fieldset', 'font', // color, face, size 'frame', 'html', // version 'hr', // align, color, noShade, size, width 'label', // form, htmlFor, control 'legend', // form, align 'li', // value, type 'map', // name, areas 'ol', // reversed, start, type, compact 'optgroup', // disabled, label 'option', 'output', 'param', // name, value, type, valueType 'pre', // width 'span', // nothing found 'table', 'tbody', 'td', 'time', // dateTime 'title', // text 'tr', 'track', 'ul', // compact, type //*/ ], //*/ } let oData = {} let oCommon = {} let aEverything = [] let oPreHash = {} let isDOMParser = true // DOMParser can't be used for these let aNoParser = [ 'link','template', // checkA 'base','basefont','caption','col','colgroup','frame','frameset','noframes','noscript','tbody','td','tfoot','th','thead','tr', // stable ] function run() { // set/reset let t0 = performance.now() oData = {} oCommon = {} aEverything = [] oPreHash = {} let tmpHash = {} let tmpPreHash = {} let oOrder = {} let perfA, perfB let splitValue = isFF ? "click" : "title" // title works for chrome + safari /* interesting (without extension fuckery which I have not tested) FF122 except for 'select', the last 310 items are the same seems to be everything after click select doesn't have 'remove' postClick actually equals span */ let tmpList = {}, totalElements = 0 for (const k of Object.keys(oList)) { let aList = oList[k] if (dom.optAll.checked) { if (oListIgnore[k] !== undefined) {aList = aList.concat(oListIgnore[k])} } totalElements += aList.length tmpList[k] = aList.sort() } const parentid = "elementsdiv" function cleanup() { try {document.getElementById(parentid).remove()} catch(e) {} } try { let htmlElement let parser = new DOMParser /* dom = 18ms cold, 10ms rerun parser = 8ms cold, 5ms rerun (so far: 8 default items still to correctly add/enumerate) so about twice as fast we could use parser where possible but DOM where we have to - we should reduce what we NEED to test first HTMLElementKeys was using a div - cydec was missing items: clientHeight clientWidth scrollHeight scrollWidth - does using the parser fix this? - are we still picking up those four items in prototype/proxy lies? - NoScript - every element had 13 items split with a space = we would still detect this */ // create one parent div to hold the others let parent = document.createElement('div') parent.setAttribute("id", parentid) document.body.appendChild(parent) // loop elements for (const k of Object.keys(tmpList).sort()) { if (k == 'STABLE A') { perfA = performance.now() - t0 } else if (k =="STABLE B") { perfB = (performance.now() - t0) - perfA } let aList = tmpList[k] tmpHash[k] = {} tmpPreHash[k] = {} aList.forEach(function(el) { let item = el.split("|")[0] let type = el.split("|")[1] let keys = [] try { // method if (isDOMParser && !aNoParser.includes(el)) { // this is faster, but some elements it seems we can't do this way /* broken in default 61* elements body, // doc.body html, // doc.all[0] meta, // doc.all[3] script, // doc.all[3] style, // doc.all[3] title, // doc.all[3] */ let tmpitem = item if (item == "title") { tmpitem = "<html><head><title></head></html>" } else if (item == "meta") { tmpitem = "<html><head><meta></head></html>" } else if (item == "script") { tmpitem = "<html></html><script>" } else if (item == "style") { tmpitem = "<html><style></html>" } let str = "<"+ tmpitem + (type == undefined ? "": " type='" + type +"'") + ">" let doc = parser.parseFromString(str, "text/html") if (item == "body") { htmlElement = doc.body } else if (item == "head") { // all elements htmlElement = doc.head } else if (item == "html") { htmlElement = doc.all[0] } else if (item == "meta" || item == "script" || item == "style" || item == "title") { htmlElement = doc.all[3] } else { htmlElement = doc.body.firstChild // firstElementChild ? } } else { const id = "target"+el const element = document.createElement(item) element.setAttribute("id", id) parent.appendChild(element) if (type !== undefined) { document.getElementById(id).type = type } htmlElement = document.getElementById(id) } for (const key in htmlElement) {keys.push(key)} // get pre + post `click` (gecko) let splitIndex = keys.indexOf(splitValue) let aPre = [], aPost = [] // what if `click` or `title` is missing: every post will be an empty array // ^ not worth coding to exclude empty arrays aPre = keys.slice(0, splitIndex) aPre.forEach(function(j) {aEverything.push(j)}) aPost = keys.slice(splitIndex, keys.length) let prehash = mini(aPre) let posthash = mini(aPost) // posthash tampering if (oCommon[posthash] == undefined) { aPost.forEach(function(j) {aEverything.push(j)}) oCommon[posthash] = {} oCommon[posthash]["data"] = aPost oCommon[posthash]["display"] = [] let aCommonTamper = [] aPost.forEach(function(r) { if (r.includes(" ")) { oCommon[posthash]["display"].push(sb + r + sc) aCommonTamper.push(r) } else { oCommon[posthash]["display"].push(r) } }) if (aCommonTamper.length) { oCommon[posthash]["tampered"] = aCommonTamper } } // prehash tampering // ^ ToDo let hash = mini(keys) // always use full keys hash so we get unique items // record each unique hash + keys + count // we need to use the full hash because two elements could have the same pre but different post if (tmpHash[k][hash] == undefined) { tmpHash[k][hash] = { "count": aPre.length, "data": keys, "elements": [], "predata": aPre, "prehash": prehash, "posthash": posthash } } tmpHash[k][hash]["elements"].push(el) // record post hashes separately if (tmpPreHash[k][prehash] == undefined) {tmpPreHash[k][prehash] = []} tmpPreHash[k][prehash].push(el) } catch(e) { console.log(el, e+"") } }) } // remove parent div cleanup() dom.perf = Math.round(performance.now() - t0) +" ms" dom.perf2 = perfA.toFixed(1) +" | " + perfB.toFixed(1) //* everything aEverything = aEverything.filter(function(item, position) {return aEverything.indexOf(item) === position}) // deduped aEverything = aEverything.filter(x => !["constructor"].includes(x)) // remove constructor aEverything.sort() // order is artifical due to htmlList so lets remove that here //console.log("EVERYTHING: " + mini(aEverything) +" ["+ aEverything.length +"]\n", "['"+ aEverything.join("','") +"']") //EVERYTHING: d6d0fc0e [630] //*/ // sort into oData by hash, and determine oOrder (by first element name) for (const k of Object.keys(tmpHash).sort()) { oData[k] = {} oOrder[k] = {} for (const j of Object.keys(tmpHash[k]).sort()) { oData[k][j] = tmpHash[k][j] let aTmp = oData[k][j].elements.sort() // sort array in oData oOrder[k][aTmp[0]] = j } } for (const k of Object.keys(tmpPreHash).sort()) { oPreHash[k] = {} for (const j of Object.keys(tmpPreHash[k]).sort()) { oPreHash[k][j] = tmpPreHash[k][j].sort() } } // display let display = [], totalResults = 0 for (const k of Object.keys(oData)) { totalResults += Object.keys(oData[k]).length } let strAll = s15 +"ALL"+ sc +": "+ s6 + mini(oData) + sc +" <span class='btn15 btnc' onclick='console.log(oData)'>[console]</span> | unique values: "+ s6 + mini(aEverything) + sc +" <span class='btn15 btnc' onclick='console.log(aEverything)'>[" + aEverything.length +"]</span> | " + totalElements +" elements | " + totalResults +" hashes<br><br><hr>" display.push(strAll) // display common let countTampered = "" let toggle = "common" display.push( "<span id='labelhidden" + toggle + "' class='btnfirst btn0' onClick=\"togglerows('hidden" + toggle +"','expand')\">[ expand ]</span> " + s15 + "COMMON"+ sc +"<br>") for (const j of Object.keys(oCommon).sort()) { let obj = oCommon[j] countTampered = obj.tampered !== undefined ? sb +" ["+ obj.tampered.length +"]"+ sc : "" display.push( s6 + j + sc + (" ["+ obj.data.length +"]").padStart(5) + countTampered + "<span class='toghidden" + toggle +" hidden faint'>[<br>" + "<span class='indent'>" + obj.display.join(", ") +"</span><br>]</span>" ) } for (const m of Object.keys(oOrder).sort()) { display.push("<br><hr>") // display pre toggle = "pre"+ m let notation = '', hash = mini(oPreHash[m]) if (isFF && !dom.optAll.checked) { let is111 = HTMLElement.prototype.hasOwnProperty("translate") let is126 = false try {is126 = "function" === typeof URL.parse} catch(e) {} if ('STABLE B' == m && is111) { /* 111 'form' (rel, relList) 108 'source' (width, height) 106 'meta' (media) 105 'canvas' 92 'slot' assign 89 'body' + frameset 75 'form' 74 'canvas' 69 'body' + frameset 68 'object', 66 'slot' assignedElements 63 'slot' assignedNodes + name 57 'body' + frameset 56 'meter' labels 56 'progress' labels 55 'style' disabled, media, type, sheet (scoped removed) */ if (hash == "1471d575") {notation = sg +' ['+ green_tick +' FF111+]'+ sc } else {notation = ' '+ zNEW } } else if ('STABLE A' == m && is126) { /* 'iframe', // 121 (loading) 'marquee', // 126 (onbounce, onfinish, onstart) 'select', // 122 showPicker 'textarea', // 116 dirName */ // bba6c822 125 if (hash == "1ae1316a") {notation = sg +' ['+ green_tick +' FF126+]'+ sc } else {notation = ' '+ zNEW } } } let preString = " | hash <span class='btn15 btnc' onclick='console.log(oPreHash[\""+ m +"\"])'>["+ hash +"]</span>" display.push( "<span id='labelhidden" + toggle + "' class='btnfirst btn0' onClick=\"togglerows('hidden" + toggle +"','expand')\">[ expand ]</span> " + s12 + m + sc + preString + notation +" | "+ tmpList[m].length +" elements | "+ Object.keys(oData[m]).length +" hashes<br>" ) for (const j of Object.keys(oOrder[m]).sort()) { let k = oOrder[m][j] let obj = oData[m][k] let commonhash = obj.posthash countTampered = "" if (oCommon[commonhash] !== undefined) { if (oCommon[commonhash].tampered !== undefined) { countTampered = sb +" ["+ oCommon[commonhash].tampered.length +"]"+ sc } } display.push( s6 + k + sc +": " + obj.prehash + (" ["+ obj.predata.length +"]").padStart(5) + " + "+ s15 + obj.posthash + sc + countTampered +": "+ obj.elements.join(", ") + "<span class='toghidden" + toggle +" hidden faint'>[<br>" + "<span class='indent'>" + obj.predata.join(", ") +"</span><br>]</span>" ) } } dom.details.innerHTML = display.join("<br>") } catch (e) { cleanup() dom.details.innerHTML = e+"" } } Promise.all([ get_globals(), ]).then(function(){ dom.optAll.checked = false run() }) </script> </body> </html> ================================================ FILE: tests/elementother.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>elements: other</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <!-- --> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 780px; max-width: 780px} /*** custom styles so we can check they are removed ***/ /* you can run check_revert() to test */ #element-fp a {margin: 2px; padding: 2px;} #element-fp b {margin: 2px; padding: 2px;} #element-fp br {margin: 2px; padding: 2px;} #element-fp code {margin: 2px; padding: 2px;} #element-fp div {margin: 2px; padding: 2px;} #element-fp hr {margin: 2px; padding: 2px;} #element-fp i {margin: 2px; padding: 2px;} #element-fp span {margin: 2px; padding: 2px;} #element-fp div>span {font-size: 2.9em;} #element-fp font[size="1"] {font-size: 1.1em;} #element-fp font[size="2"] {font-size: 1.2em;} #element-fp font[size="3"] {font-size: 1.3em;} #element-fp font[size="4"] {font-size: 1.4em;} #element-fp font[size="5"] {font-size: 1.5em;} #element-fp font[size="6"] {font-size: 1.6em;} #element-fp font[size="7"] {font-size: 1.7em;} #element-fp h1 {margin: 2px; padding: 2px;} #element-fp h2 {margin: 2px; padding: 2px;} #element-fp h3 {margin: 2px; padding: 2px;} #element-fp h4 {margin: 2px; padding: 2px;} #element-fp h5 {margin: 2px; padding: 2px;} #element-fp h6 {margin: 2px; padding: 2px;} #element-fp p {margin: 2px; padding: 2px;} #element-fp small {margin: 2px; padding: 2px;} #element-fp sub {margin: 2px; padding: 2px;} #element-fp sup {margin: 2px; padding: 2px;} abbr, acronym, address, applet, article, aside, audio, base, basefont, big, blockquote, caption, center, cite, data, dd, del, dfn, dialog, dir, dl, dt { margin: 2px; padding: 2px; } em, figcaption, fieldset, figure, footer, form, geolocation, header, hgroup, ins, isindex, keygen, kbd, label, legend, li, link, main, mark, marquee, menu, nav, nobr, noframes { margin: 2px; padding: 2px; } ol, optgroup, option, output, plaintext, portal, pre, q, rb, rbc, rp, rt, rtc, ruby, s, samp, search, section, slot, strike, strong, summary, time, title, tt, u, ul, var, xmp { margin: 2px; padding: 2px; } canvas, iframe, img, meter, object, picture, video { width: 75px; } /* don't think I can affect the style of these with JS enabled noembed, noscript = 0x0 */ #element-fp noembed {border: 20px; padding: 2px;} #element-fp noscript {bortder: 20px; padding: 2px;} </style> </head> <body> <div id="element-fp"></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#elements">return to TZP index</a></td></tr> </table> <table id="tb15"> <col width="25%"><col width="75%"> <thead><tr><th colspan="2"> <div class="nav-title">elements: other <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="perf" id="locale"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Element sizes not covered in <a class="blue" target="_blank" href="elementforms.html">element forms</a>. Counts for unique elements across the two tests are not directly comparable. In everything all elements are treated equally with both <code>writing-modes</code>, which is not the case in the TZPtest. </span><br><br> <span class="btn15 btnfirst" onClick="run(`everything`)">[ everything ]</span> <span class="btn15 btnfirst" onClick="run(`TZP`)">[ TZP ]</span> <span class="btn15 btnfirst" onClick="list_elements()">[ elements ]</span> <span class="btn15 btnfirst"> <a class="blue" target="_blank" href="elementother_nocss.html" onClick="run('everything')">[ NO CSS TEST ]</a> </span> </td></tr> <tr><td colspan="2"><hr><br></td></tr> <tr><td colspan="2" style="text-align: left;"> <div class="c mono spaces no_color" id="elements"></div> </td></tr> </table> <br> <script> 'use strict'; /* changing app language has no effect (the only char we're using is '.') otherwise looks like everything is pretty much fixed/hardcoded/set due to widgets and defaults - so this is exposing subpixels (and version changes) only IIUIC edit: except maybe french and quote */ let strElement = '', isRevert = true let oData = {}, oList = {}, oListBase = {}, oTZP = {}, oChecks = {}, oCheckSummary = {} let testTarget let aRemainder = [] let aIgnored = [ // ignore 'area', // used in map and sets it's own size 'bdi','bdo', //: bidirectional elements 'col','colgroup', 'datalist', 'element-details', // https://mdn.github.io/web-components-examples/element-details/ 'form', // all forms covered in another metric 'frame','frameset', // deprecated 'head', // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/head 'map', 'menuitem', // deprecated 'nextid', // died 1997 with HTML v3.2 'param', // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/param 'script', 'source', 'style', 'template', // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/template 'track', // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/track 'wbr', // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/wbr ] let aUseFirstChild = ['hgroup'] // we want to measure the first element, not the last let oExtraStyles = { 'marquee': '; width: 20px; height: 20px', // if we don't constrain it, it changes with inner window sizes } function build_lists() { // note: some elements we insert a char "." to a) force a height // or b) for unique measurements without a char to get more precision/decimal places // always use the same char let oTmp = { a: '<a href="">.</a>', audio: '<audio controls=""></audio>', base: '<base href=""/>', // empty: width/x are zero but height/y are interesting basefont: '<basefont color="#FF0000"/>', big_x2: '<big><big>.</big></big>', big_x3: '<big><big><big>.</big></big></big>', // always revert tables because we have that in our inline style in the plain test caption: '<table><caption>.</caption></table>', dd: '<dl><dd>.</dd></dl>', dialog: '<dialog open=""></dialog>', dt: '<dl><dt>.</dt></dl>', figcaption: '<figure><figcaption>.</figcaption></figure>', hgroup: '<hgroup><h1>.</h1><p class="revert">.</p></hgroup>', // revert 2nd child (p) isindex: '<isindex prompt="search"></isindex>', keygen: '<form method="POST"><keygen></keygen></form>', label: '<div id="elementlabel"><label for="elementlabel">.</label></div<', legend: '<fieldset><legend>.</legend></fieldset>', li: '<ul><li></li></ul>', link: '<link href="" rel="stylesheet">', noembed: '<embed><noembed>.</noembed>', output: '<form><input>=<output class="revert"></output></form>', // '<form><input id="a" value="."> = <output id="x" name="x" for="a"></output></form>', // we don't actually need to calculate any values rb: '<ruby><rb>.</rb></ruby>', rbc: '<ruby><rbc>.</rbc></ruby>', rp: '<ruby><rp>.</rp></ruby>', rt: '<ruby><rt>.</rt></ruby>', rtc: '<ruby><rtc>.</rtc></ruby>', summary: '<details><summary>.</summary></details>', 'q_empty': '<q></q>', // tables tbody: '<table><tbody></tbody></table>', td: '<table><tr><td></td></tr></table>', tfoot: '<table><tfoot></tfoot></table>', th: '<table><thead><tr><th></th></tr></thead></table>', thead: '<table><thead></thead></table>', tr: '<table><tr></tr></table>', // other 'ol_li': '<ol><li>.</li></ol>', 'menu_li': '<menu>.<li></li></menu>', 'ul_li': '<ul><li>.</li></ul>', // test error //'error': '<frame></frame>' } // programatically add more let oExtra = { 'char': [ 'abbr','acronym','address','applet','article','aside', 'b','big','blockquote','center','cite','code', 'data','del','dir','div','dfn','dl','em','footer', 'h1','h2','h3','h4','h5','h6','header', 'i','ins','iframe','kbd', 'main','mark','marquee','menu','meter', 'nobr','noframes','noscript','ol','option','p','pre','q', 'ruby', 's','samp','section','slot','small','span','strike','strong','sub','sup', 'time','title','tt', 'u','ul','var','xmp', ], nochar: [ 'canvas','fieldset','figure','geolocation','img', 'nav','optgroup','picture', 'portal', // https://wicg.github.io/portals/#the-portal-element 'search','table','video', ], standalone: [ 'br','hr','object','plaintext', ] } for (const type of Object.keys(oExtra)) { for (const k of Object.keys(oExtra[type])) { let array = oExtra[type] array.forEach(function(item) { if ('standalone' == type) { oTmp[item] = '<'+ item +'>' } else { let str = 'char' == type ? '.' : '' oTmp[item] = '<'+ item +'>'+ str +'</'+ item +'>' } }) } } // sort into final object for (const k of Object.keys(oTmp).sort()) { oListBase[k] = oTmp[k] } // build TZP // we can't get more unique elements than actual elements used // so don't double up: e.g. in blink android we have 35 unique elements in // everything but only 34 elements used in tzp (at the time): so we need to // add a new element: the point of difference was in "base, basefont, picture" // (grouped in everything) - we already use base, so instead add basefont or picture oTmp = { 'horizontal-tb': ['base','figure'], 'vertical-lr': [ 'a','audio','b','base','big','big_x2','big_x3','blockquote','br', 'canvas','caption','code','dd','dialog','dl','dt','fieldset','figcaption', 'geolocation','h1','h2','h3','h4','h5','h6','hgroup','hr','i','iframe','img', 'legend','li','marquee','menu_li','meter','noembed','noscript', 'ol_li','optgroup','option','output','plaintext','pre','q','q_empty', 'rt','small','sub','sup','tfoot','td','ul', ] } // populate for (const t of Object.keys(oTmp).sort()) { oTZP[t] = {} let array = oTmp[t].sort() array.forEach(function(item) {oTZP[t][item] = oListBase[item]}) } } function create_element(str, vertical = true, isLoop = false) { let style = vertical ? 'vertical-lr' : 'horizontal-tb' oList = {} oList[style] = {'test': str} let oRes = get_elements('everything', true) // returned tmpdata, target let itemdata = oRes[0]['test'][style.slice(0,-3)] if (!isLoop) { testTarget = oRes[1] console.log('testTarget', style, '\n', testTarget) console.log('isRevert', isRevert, mini(itemdata), itemdata) } else { return [mini(itemdata), itemdata] } } function get_elements(type, isTest = false) { if (undefined == type) {type = 'TZP'} let t0 = performance.now() oData = {} let params = [s15 + type + sc] if (!isTest) { oList = 'TZP' == type ? oTZP : {'horizontal-tb': oListBase, 'vertical-lr': oListBase} } let tmpdata = {}, newobj = {} let target, testCount = 0, shadowData = {} const parent = dom['element-fp'] try { // count unique hashes per element+writingstyle + unique element names let setHash = new Set(), setElements = new Set() // create a shadow data object to track unique elements // note: it would be easy to create misleading uniqueness given // 30+ unqiue elements in everything x 2 writing-modes but // we can be diligent in spotting points of difference (e.g. see // noembed vertical for blink) and it would be nice to match up // the numbers across the two tests // ^ use shadowData for (const s of Object.keys(oList).sort()) { let style = s.slice(0,-3) if ('TZP' == type) {tmpdata[style] = {}} for (const k of Object.keys(oList[s]).sort()) { let itemdata = {} // count measurments taken testCount++ // unique elements tested setElements.add(k) // set parent, determine target to measure and as we walk // the children, ensure no other css affects any element //parent.innerHTML = '' parent.innerHTML = oList[s][k] try { target = parent.firstChild // revert everything for (let i = 0; i < 10; i++) { if (isRevert) {target.classList.add('revert')} let newtarget = target.children[0] if (undefined == newtarget) {break} target = newtarget } // choose target if (aUseFirstChild.includes(k)) {target = parent.firstChild} // set style let extraStyle = undefined == oExtraStyles[k] ? '' : oExtraStyles[k] target.setAttribute("style","display:inline; writing-mode: "+ s + extraStyle +";") let method = target.getBoundingClientRect() itemdata = [method.width, method.height, method.x, method.y] } catch(e) { itemdata = zErr //console.log(k, e+'') } if ('TZP' == type) { let itemhash = mini(itemdata) // add shadowData if (undefined == shadowData[k]) {shadowData[k] = {}} shadowData[k][style] = itemdata // unique measurments setHash.add(itemhash) // record if (undefined == tmpdata[style][itemhash]) {tmpdata[style][itemhash] = {'data': itemdata, 'group': [k]} } else {tmpdata[style][itemhash]['group'].push(k)} } else { if (undefined == tmpdata[k]) {tmpdata[k] = {}} tmpdata[k][style] = itemdata } } } if (isTest) { return [tmpdata, target] } // counts let aElements = Array.from(setElements) params.push(aElements.length +' elements') params.push(testCount +' measurements') //console.log(tmpdata) let aHash if ('TZP' == type) { // group by results for (const s of Object.keys(tmpdata)) { newobj[s] = {} for (const k of Object.keys(tmpdata[s])) { let keydata = tmpdata[s][k].group.sort() newobj[s][keydata.join(' ')] = tmpdata[s][k]['data'] } } for (const s of Object.keys(newobj)) { oData[s] = {} for (const k of Object.keys(newobj[s]).sort()) {oData[s][k] = newobj[s][k]} } aHash = Array.from(setHash) params.push(aHash.length +' unique measurements') // shadowData //console.log(shadowData) let setShadow = new Set() for (const k of Object.keys(shadowData)) { let elementhash = mini(shadowData[k]) setShadow.add(elementhash) } let aShadow = Array.from(setShadow) params.push(aShadow.length +' unique elements') } else { // group by element hash for (const k of Object.keys(tmpdata)) { let elementhash = mini(tmpdata[k]) setHash.add(elementhash) if (undefined == newobj[elementhash]) {newobj[elementhash] = {'data': tmpdata[k], 'elements': [k]} } else {newobj[elementhash]['elements'].push(k)} } // for (const k of Object.keys(newobj)) { // don't sort oData[k] = { 'elements': newobj[k]['elements'].join(', '), 'data': newobj[k]['data'] } } aHash = Array.from(setHash) params.push(aHash.length +' unique elements') } //console.log(tmpdata) parent.innerHTML = "" // clear display let hash = mini(oData), notation = "" let t1 = performance.now() try {dom.perf.innerHTML = Math.round(t1 - t0) +" ms"} catch(e) {} let strParam = (params.length ? " ["+ params.join(" | ") +"]" : "") let display = [], tmphash = "" dom.elements.innerHTML = hash + strParam +"<br><br>"+ json_highlight(oData, 120) return } catch(e) { parent.innerHTML = "" // clear display dom.elements = e+"" return } } function run(type) { // clear dom.elements.innerHTML = " &nbsp; " // pause so users see change setTimeout(function() { get_elements(type) }, 170) } Promise.all([ get_globals(), build_lists(), ]).then(function(){ isRevert = true run_once() run() }) /*** NOT USED in NOCSS test ***/ function run_once() { // display lang/locale let isLanguage = '', isLocale = '' try {isLocale = Intl.DateTimeFormat().resolvedOptions().locale} catch(e) {isLocale = zErr} try {isLanguage = navigator.language} catch(e) {isLanguage = zErr} try {dom.locale.innerHTML = isLanguage +' | '+ isLocale} catch(e) {} // enumerate elements: messy as F but who cares .. track element use and display let sKey = '<span class="key">', sNumber = '<span class="number">', sString = '<span class="string">' let tzpSet = new Set(), aAll = [], aRedundant = [] for (const t of Object.keys(oTZP)) {for (const k of Object.keys(oTZP[t])) {tzpSet.add(k)}} let aTZP = Array.from(tzpSet).sort() for (const k of Object.keys(oListBase)) {if (!aTZP.includes(k)) {aRedundant.push(k)}} let oElements = { 'ignored': aIgnored.sort(), 'redundant': aRedundant.sort(), 'tzp': aTZP.sort(), 'remainder': aRemainder.sort(), } for (const k of Object.keys(oElements)) {aAll = aAll.concat(oElements[k])} aAll.sort() let aAllCheck = aAll.filter(function(item, position) {return aAll.indexOf(item) === position}) let sanityStr = '' if (aAll.length !== aAllCheck.length) { sanityStr = sb+' [duplicates]'+sc console.log('duplicates\n', aAll) } let countTotal = aRedundant.length + aTZP.length + aIgnored.length + aRemainder.length strElement = '<u>ELEMENTS</u> [' + countTotal +']' + sanityStr + '<br><br>' strElement += '{<br><div class="indent">'+ sKey +'ignored: ' + sc +'{ ' + sNumber + aIgnored.length + sc +'<br><div class="indent">'+ sString + aIgnored.join(', ') + sc +'</div><br>}' +'</div><br>}' strElement += '<br>{<br><div class="indent">'+ sKey +'redundant: ' + sc +'{ ' + sNumber + aRedundant.length + sc +'<br><div class="indent">'+ sString + aRedundant.join(', ') + sc +'</div><br>}' +'</div><br>}' strElement += '<br>{<br><div class="indent">'+ sKey +'TZP: ' + sc +'{ ' + sNumber + aTZP.length + sc +'<br><div class="indent">'+ sString + aTZP.join(', ') + sc +'</div><br>}' +'</div><br>}' if (aRemainder.length) { strElement += '<br>{<br><div class="indent">'+ sKey +'TBA:: work in progres: ' + sc +'{ ' + sNumber + aRemainder.length + sc +'<br><div class="indent">'+ sString + aRemainder.join(', ') + sc +'</div><br>}' +'</div><br>}' } } function list_elements() { dom.perf.innerHTML = '' dom.elements.innerHTML = strElement } function check_revert(vertical = true) { // with our inline styles messing with things everything should be false (no matches) // if we comment out the inline styles, everything should match let style = vertical ? 'vertical' : 'horizontal' // remember isRevert let isRevertState = isRevert // reset oChecks = {} oCheckSummary = {} // cycle thru each element and measure with and // without revert to ensure we are changing it for (const k of Object.keys(oListBase)) { let str = oListBase[k] isRevert = true let data = create_element(str, vertical, true) let hashR = data[0] oChecks[k] = { 'normal': '', 'revert': data, } isRevert = false data = create_element(str, vertical, true) let hashN = data[0] oChecks[k].normal = data let test = hashR == hashN if (undefined == oCheckSummary[test]) {oCheckSummary[test] = []} oCheckSummary[test].push(k) } console.log(style) console.log('summary\n' ,oCheckSummary) console.log('data\n', oChecks) // restore isRevert isRevert = isRevertState } </script> </body> </html> ================================================ FILE: tests/elementother_nocss.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>elements: other [no css]</title> <!--<link rel="stylesheet" type="text/css" href="testindex.css"> <!-- --> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> .mono {font-family: monospace, "Courier New"; font-size: 12px;} .spaces {white-space: pre-wrap;} #tb15 { width: 97%; min-width: 780px; max-width: 780px; } #element-fp { position: fixed; top: 0; left: 0; font-family: none; font-size: initial; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; text-transform: none; text-align: left; text-decoration: none; text-shadow: none; white-space: nowrap; transform: skew(1.787542deg,3.263901deg); } </style> </head> <body> <div id="element-fp"></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#elements">return to TZP index</a></td></tr> </table> <table id="tb15"> <col width="25%"><col width="75%"> <thead><tr><th colspan="2"> <div class="nav-title">elements: other [no css]</div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span>The <a href="elementother.html">elements: other</a> hash should match the one here - to ensure that custom styles don't affect the fingerprint.</span><br><br> <span class="btn15 btnfirst" style="cursor: pointer;" onClick="run(`everything`)">[ everything ]</span> </td></tr> <tr><td colspan="2"><hr><br></td></tr> <tr><td colspan="2" style="text-align: left;"> <div class="c mono spaces no_color" id="elements"></div> </td></tr> </table> <br> <script> 'use strict'; let isRevert = false let oData = {}, oList = {}, oListBase = {} let testTarget let aUseFirstChild = ['hgroup'] // we want to measure the first element, not the last let oExtraStyles = { 'marquee': '; width: 20px; height: 20px', // if we don't constrain it, it changes with inner window sizes } function build_lists() { // note: some elements we insert a char "." to a) force a height // or b) for unique measurements without a char to get more precision/decimal places // always use the same char let oTmp = { a: '<a href="">.</a>', audio: '<audio controls=""></audio>', base: '<base href=""/>', // empty: width/x are zero but height/y are interesting basefont: '<basefont color="#FF0000"/>', big_x2: '<big><big>.</big></big>', big_x3: '<big><big><big>.</big></big></big>', // always revert tables because we have that in our inline style in the plain test caption: '<table><caption>.</caption></table>', dd: '<dl><dd>.</dd></dl>', dialog: '<dialog open=""></dialog>', dt: '<dl><dt>.</dt></dl>', figcaption: '<figure><figcaption>.</figcaption></figure>', hgroup: '<hgroup><h1>.</h1><p class="revert">.</p></hgroup>', // revert 2nd child (p) isindex: '<isindex prompt="search"></isindex>', keygen: '<form method="POST"><keygen></keygen></form>', label: '<div id="elementlabel"><label for="elementlabel">.</label></div<', legend: '<fieldset><legend>.</legend></fieldset>', li: '<ul><li></li></ul>', link: '<link href="" rel="stylesheet">', noembed: '<embed><noembed>.</noembed>', output: '<form><input>=<output class="revert"></output></form>', // '<form><input id="a" value="."> = <output id="x" name="x" for="a"></output></form>', // we don't actually need to calculate any values rb: '<ruby><rb>.</rb></ruby>', rbc: '<ruby><rbc>.</rbc></ruby>', rp: '<ruby><rp>.</rp></ruby>', rt: '<ruby><rt>.</rt></ruby>', rtc: '<ruby><rtc>.</rtc></ruby>', summary: '<details><summary>.</summary></details>', 'q_empty': '<q></q>', // tables tbody: '<table><tbody></tbody></table>', td: '<table><tr><td></td></tr></table>', tfoot: '<table><tfoot></tfoot></table>', th: '<table><thead><tr><th></th></tr></thead></table>', thead: '<table><thead></thead></table>', tr: '<table><tr></tr></table>', // other 'ol_li': '<ol><li>.</li></ol>', 'menu_li': '<menu>.<li></li></menu>', 'ul_li': '<ul><li>.</li></ul>', // test error //'error': '<frame></frame>' } // programatically add more let oExtra = { 'char': [ 'abbr','acronym','address','applet','article','aside', 'b','big','blockquote','center','cite','code', 'data','del','dir','div','dfn','dl','em','footer', 'h1','h2','h3','h4','h5','h6','header', 'i','ins','iframe','kbd', 'main','mark','marquee','menu','meter', 'nobr','noframes','noscript','ol','option','p','pre','q', 'ruby', 's','samp','section','slot','small','span','strike','strong','sub','sup', 'time','title','tt', 'u','ul','var','xmp', ], nochar: [ 'canvas','fieldset','figure','geolocation','img', 'nav','optgroup','picture', 'portal', // https://wicg.github.io/portals/#the-portal-element 'search','table','video', ], standalone: [ 'br','hr','object','plaintext', ] } for (const type of Object.keys(oExtra)) { for (const k of Object.keys(oExtra[type])) { let array = oExtra[type] array.forEach(function(item) { if ('standalone' == type) { oTmp[item] = '<'+ item +'>' } else { let str = 'char' == type ? '.' : '' oTmp[item] = '<'+ item +'>'+ str +'</'+ item +'>' } }) } } // sort into final object for (const k of Object.keys(oTmp).sort()) { oListBase[k] = oTmp[k] } } function create_element(str, vertical = true) { let style = vertical ? 'vertical-lr' : 'horizontal-tb' oList = {} oList[style] = {'test': str} let oRes = get_elements('everything', true) // returned tmpdata, target let itemdata = oRes[0]['test'][style.slice(0,-3)] testTarget = oRes[1] console.log('testTarget', style, '\n', testTarget) console.log('isRevert', isRevert, mini(itemdata), itemdata) } function get_elements(type, isTest = false) { type = 'everything' let t0 = performance.now() oData = {} let params = [s15 + type + sc] if (!isTest) { oList = {'horizontal-tb': oListBase, 'vertical-lr': oListBase} } let tmpdata = {}, newobj = {} let target, testCount = 0, shadowData = {} const parent = dom['element-fp'] try { // count unique hashes per element+writingstyle + unique element names let setHash = new Set(), setElements = new Set() // create a shadow data object to track unique elements // note: it would be easy to create misleading uniqueness given // 30+ unqiue elements in everything x 2 writing-modes but // we can be diligent in spotting points of difference (e.g. see // noembed vertical for blink) and it would be nice to match up // the numbers across the two tests // ^ use shadowData for (const s of Object.keys(oList).sort()) { let style = s.slice(0,-3) for (const k of Object.keys(oList[s]).sort()) { let itemdata = {} // count measurments taken testCount++ // unique elements tested setElements.add(k) // set parent, determine target to measure and as we walk // the children, ensure no other css affects any element //parent.innerHTML = '' parent.innerHTML = oList[s][k] try { target = parent.firstChild // revert everything for (let i = 0; i < 10; i++) { if (isRevert) {target.classList.add('revert')} let newtarget = target.children[0] if (undefined == newtarget) {break} target = newtarget } // choose target if (aUseFirstChild.includes(k)) {target = parent.firstChild} // set style let extraStyle = undefined == oExtraStyles[k] ? '' : oExtraStyles[k] target.setAttribute("style","display:inline; writing-mode: "+ s + extraStyle +";") let method = target.getBoundingClientRect() itemdata = [method.width, method.height, method.x, method.y] } catch(e) { itemdata = zErr //console.log(k, e+'') } if (undefined == tmpdata[k]) {tmpdata[k] = {}} tmpdata[k][style] = itemdata } } if (isTest) { return [tmpdata, target] } // counts let aElements = Array.from(setElements) params.push(aElements.length +' elements') params.push(testCount +' measurements') //console.log(tmpdata) let aHash // group by element hash for (const k of Object.keys(tmpdata)) { let elementhash = mini(tmpdata[k]) setHash.add(elementhash) if (undefined == newobj[elementhash]) {newobj[elementhash] = {'data': tmpdata[k], 'elements': [k]} } else {newobj[elementhash]['elements'].push(k)} } // for (const k of Object.keys(newobj)) { // don't sort oData[k] = { 'elements': newobj[k]['elements'].join(', '), 'data': newobj[k]['data'] } } aHash = Array.from(setHash) params.push(aHash.length +' unique elements') //console.log(tmpdata) parent.innerHTML = "" // clear display let hash = mini(oData), notation = "" let t1 = performance.now() try {dom.perf.innerHTML = Math.round(t1 - t0) +" ms"} catch(e) {} let strParam = (params.length ? " ["+ params.join(" | ") +"]" : "") let display = [], tmphash = "" dom.elements.innerHTML = hash + strParam +"<br><br>"+ json_highlight(oData, 120) return } catch(e) { parent.innerHTML = "" // clear display dom.elements = e+"" return } } function run(type) { // clear dom.elements.innerHTML = " &nbsp; " // pause so users see change setTimeout(function() { get_elements(type) }, 170) } Promise.all([ get_globals(), build_lists(), ]).then(function(){ isRevert = false run() }) </script> </body> </html> ================================================ FILE: tests/engine.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=700"> <title>engine</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} #tb3 td:first-child { text-align: left; vertical-align: top;} </style> </head> <body> <div class="offscreen"> <div id="mozFont"></div> <div id="test95a" style="width: min-content; hyphens: auto; border: 1px solid red">2020-1</div> <div id="test95b" style="width: min-content; hyphens: auto; border: 1px solid red">2020-12020-1</div> </div> <div class="hidden"> <div><input type="time" min="14:00:00" max="12:00:00" value="15:00:00" id="test76"></div> <span id="mozColor"></span> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#feature">return to TZP index</a></td></tr> </table> <table id="tb3"> <col width="50%"><col width="50%"> <thead><tr><th colspan="2"> <div class="nav-title">engine <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A tiny sample of the myriad ways engines differ. This test is tweaked for gecko [FF52+]: you want <span class="s9"><b> &#x2713; </b></span>'s. Anything different <span class="bad"><b> &#x2716; </b></span> or blocked <span class="s4"><b> &#x2715; </b></span> only makes you stand out. A single unspoofable property check can take as little as <span class="s3">0.01 ms</span>. </span> </td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2"><span class="no_color c mono spaces" id="results"></span></td></tr> </table> <br> <script> 'use strict'; let results = [], itemnumbers = [], data = {}, aPerf = [], count = 0, expected = 96, gt0, // global timer bBlock = false, // throw a test error bNum = false // display numbering instead of perf function fnTrim(str, len) { str = str.replace(/(\r\n|\n|\r)/gm,"") if (str.length > len) {str = str.slice(0,len-3) +"..."} return str } function fnClean(str) { if (str == "") {str = "empty string" } else if (str === undefined) {str = zU } else if (str == zU) {str = zUQ} return str } function logPerf() { if (aPerf.length) { console.log("PERF LOG\n", aPerf) } } function fnDisplay() { // perf let perfValue = Math.round(performance.now() - gt0) +" ms" let perfTop = perfValue // remove the first 5 items: section headers + hr if (aPerf.length) { perfTop = "<span style='cursor: pointer;' onClick='logPerf()'>" + perfTop +"</span>" aPerf.splice(0, 5) } dom.perf.innerHTML = perfTop // data -> ordered results const names = Object.keys(data).sort((a,b) => a-b) for (const k of names) {results.push(k +"~~~"+ data[k])} let display = [], errcount = 0 let countTrue = 0, countFalse = 0, countBlock = 0, countNA = 0 // check no numbers duplicated if (expected !== results.length) { display.push(sb+ "ERROR:"+ sc + " duplicate numbers detected<br>") } // parse for pretty output for (let i=0; i < results.length; i++) { let pad = 35 let order = results[i].split("~~~")[0], desc = results[i].split("~~~")[1].trim(), result = results[i].split("~~~")[2], match = results[i].split("~~~")[3], perf = results[i].split("~~~")[4] if (perf == undefined || perf == "" || perf == "undefined") {perf = " - " } else if (perf !== "NaN") { perf = Math.round(perf) perf = perf.toString() perf = perf.padStart(2) +" " } if (desc == "header") { let note = "" if (result == "errors") { note = " [there are thousands of these]" } result = s14 + result.toUpperCase() + sc + note display.push("<span>"+ result + "</span><br>") } else if (desc == "hr") { display.push("<br><hr>") } else { if (match == zErr) {match = yellow_block; countBlock++ } else if (match == "true") {match = green_tick; countTrue++ } else if (match == "false") {match = red_cross; countFalse++ } else {match = white_na; countNA++} match += " " if (desc == "error") { pad = 3; errcount++; desc = errcount.toString() } desc = s3 + desc.padStart(pad) + sc let sectionspace = "" if (i+1 < results.length) { let nextitem = results[i+1].split("~~~")[1].trim() if (nextitem == "header") {sectionspace = "<br>"} } desc += s99 +" "+ (bNum ? order : perf) + sc display.push(desc + match +" "+ result + sectionspace) } } // summary let countValid = countTrue + countFalse + countBlock // na tests ignored let countFail = countFalse + countBlock let percentPassed = Math.floor((countTrue/countValid)*100) let isPass = percentPassed == 100 ? true : false let percentFailed = Math.ceil((countFail/countValid)*100) let summary = "" if (isFFvalid) { let needStr = "... who needs "+ s3+ (expected - 5) + " tests" + sc + " and "+ s3 + "several hundred metrics" + sc + " in "+ s3 + perfValue + sc + "?" summary += s14 +"MINI TESTS "+ sc + needStr +"<br><br>" + "engine: "+ isEnginePretty.trim() +"<br><br>" + " gecko: "+ isFFpretty.trim() +"<br><br><hr><br>" } summary += s14 +"SUMMARY "+ sc +"PASS: ".padStart(7) + countTrue + (isPass? sg : sb) +" ["+ percentPassed +"%]"+ sc +"FAIL: ".padStart(7) + countFail + (isPass? sg : sb) +" ["+ percentFailed +"%]"+ sc if (countNA > 0) {summary += "N/A: ".padStart(6) + countNA} if (isFF) { summary += s14+ "DEBUG".padStart(7) + sc let strVer = isVer strVer += (isVer == isVerMax ? "+" : "") strVer += (isVer == 52 ? " or lower" : "") summary += "VER: ".padStart(6) + strVer } summary += " " + buildButton(3, "all", "all details") // output dom.results.innerHTML = summary + "<br><br>" + display.join("<br>") } function fnRecord(order, description, result, match, error, perf) { if ("number" === typeof perf) { perf = performance.now() - perf } if (error !== undefined && error !== "skip") { let hash = mini(error) if (hash !== "d8281b3c") { // assignment to undeclared variable bB //console.error(order, result, error) hash = s3+ " ["+ hash +"]"+ sc } else { result = "test error thrown"; hash = "" } result += hash } aPerf.push([perf, order, description]) order = (order+"").padStart(3,"0") data[order] = description +"~~~"+ result +"~~~"+ match + "~~~" + perf count ++ itemnumbers.push(order +" "+ description) if (count == expected) { fnDisplay() } if (count > expected) {console.error(count, "expected count too low")} } function get_colorgamut(num, title) { // added in FF110+ 1422237 let q = "(color-gamut: ", res = "undefined", bolC = false try { let t0 = performance.now() if (bBlock) {bB = 1} if (window.matchMedia(q +"srgb)").matches) {res = "srgb"} if (window.matchMedia(q +"p3)").matches) {res = "p3"} if (window.matchMedia(q +"rec2020)").matches) {res = "rec2020"} let bool = res == "undefined" if (isVer > 109) { bool = zNA res = zNA +": FF109 or lower "+ s3 +"["+ res +"]"+ sc } else if (!isFF) { bool = zNA res += " [FF109 or lower]" } fnRecord(num, title, res, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_dates(num) { // 1274354: meta // 1557650 let strA = "[5 digit year] new Date" try { let t0A = performance.now() if (bBlock) {bB = 1} let resA = new Date("19999-11-11") let bolA = resA == "Invalid Date" ? true : false if (!bolA) {resA = fnTrim(resA.toString(), 40)} if (isVer > 119) { bolA = zNA resA = zNA +": FF119 or lower " + s3 +"["+ fnTrim(resA.toString(), 23) +"]" + sc } else if (!isFF) { bolA = zNA resA += " [FF119 or lower]" } fnRecord(num, strA, resA, bolA, "skip", t0A) } catch(e) { fnRecord(num, strA, e.name, zErr, e.message) } // 1515318 let strB = "[hyphen] new Date" try { let t0B = performance.now() if (bBlock) {bB = 1} let resB = new Date("31-Mar-2011").getFullYear() // FF: 63 and lower = NaN, 64+ = -2011 let bolB = false if (resB == -2011) {bolB = true } else if (isVer < 64 && isNaN(resB)) {bolB = true } else if (isVer > 112 && resB == 2011) { bolB = zNA resB = zNA +": FF112 or lower " + s3 +"["+ resB+"]" + sc } else if (!isFF) { bolB = zNA resB += " [FF112 or lower]" } fnRecord(num+1, strB, resB, bolB, "skip", t0B) } catch(e) { fnRecord(num+1, strB, e.name, zErr, e.message) } // 1439800 let strC = "[string] new Data" try { let t0C = performance.now() if (bBlock) {bB = 1} let resC = new Date("11-Nov-11") let bolC = resC.toString() == "Invalid Date" ? true : false if (!bolC) { if (isFF) { if (isVer > 112) { bolC = zNA resC = zNA +": FF112 or lower " + s3 +"["+ fnTrim(resC.toString(), 23) +"]"+sc } else { resC = fnTrim(resC.toString(), 40) } } else { bolC = zNA resC = fnTrim(resC.toString(), 22) +" [FF112 or lower]" } } fnRecord(num+2, strC, resC, bolC, "skip", t0C) } catch(e) { fnRecord(num+2, strC, e.name, zErr, e.message) } // 1599375 let strD = "[+0000 UTC] Date.parse" try { let t0D = performance.now() if (bBlock) {bB = 1} let resD = Date.parse("2019-11-26 07:39:58.286157072 +0000 UTC") let bolD = "" if (isVer > 120) { bolD = zNA resD = zNA +": FF120 or lower " + s3 +"["+ fnTrim(resD.toString(), 23) +"]"+sc } else { bolD = isNaN(resD) ? true : false } fnRecord(num+3, strD, resD, bolD, "skip", t0D) } catch(e) { fnRecord(num+3, strD, e.name, zErr, e.message) } // 1730155 let strE = "[localized] Date.parse" try { let t0E = performance.now() if (bBlock) {bB = 1} let resE = Date.parse("Mercredi 8 Septembre 2021") let bolE = "" if (isVer > 121) { // awaiting v122 test bolE = zNA resE = zNA +": FF121 or lower " + s3 +"["+ fnTrim(resE.toString(), 23) +"]"+sc } else { bolE = isNaN(resE) ? true : false } fnRecord(num+4, strE, resE, bolE, "skip", t0E) } catch(e) { fnRecord(num+4, strE, e.name, zErr, e.message) } // 1783731 let strF = "[subtraction] Date.parse" try { let t0F = performance.now() if (bBlock) {bB = 1} let resF = Date.parse('2022-08-08') - Date.parse('2022-08-8') let bolF = "" if (isVer > 120) { bolF = zNA resF = zNA +": FF120 or lower " + s3 +"["+ fnTrim(resF.toString(), 23) +"]"+sc } else { bolF = resF == 0 } fnRecord(num+6, strF, resF, bolF, "skip", t0F) } catch(e) { fnRecord(num+6, strF, e.name, zErr, e.message) } } function get_errors(num) { let tests = [ ["var a = {}; a.b = a; JSON.stringify(a)", "TypeError: cyclic object value"], ["alert('A)","SyntaxError: '' string literal contains an unescaped line break"], ["let a = 1_00_;","SyntaxError: underscore can appear only between digits, not after t...", // FF72+ "SyntaxError: identifier starts immediately after numeric literal", // FF60-69 "SyntaxError: missing digit after '_' numeric separator"], // FF70-71 ["const foo;foo.bar","SyntaxError: missing = in const declaration"], ["null.bar","TypeError: can't access property \"bar\" of null","TypeError: null has no properties"], ["(1).toString(1000)","RangeError: radix must be an integer at least 2 and no greater than 36"], ["var x = new Array(-1)","RangeError: invalid array length"], ["[...undefined].length","TypeError: can't access property Symbol.iterator of undefined","TypeError: undefined has no properties"], ["const tzp=1; const tzp=2;","SyntaxError: redeclaration of const tzp"], ["const foo;foo.bar","SyntaxError: missing = in const declaration"], ["var x = @","SyntaxError: illegal character U+0040","SyntaxError: illegal character"], ] tests.forEach(function(array) { let t0 = performance.now() try { newFn(array[0]) } catch(e) { let bool = false let str = fnTrim(e.name +": "+ e.message, 70) if (isVer < 60 && array[0] == "alert('A)") { if (str == "SyntaxError: unterminated string literal") {bool = true} } else { if (str == array[1] || str == array[2] || str == array[3]) {bool = true} } fnRecord(num, "error", str, bool, "skip", t0) num++ } }) } function get_eval_length(num, title) { let bool = false try { let t0 = performance.now() if (bBlock) {bB = 1} let test = eval.toString().length if (test == 37) {bool = true} fnRecord(num, title, test, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_iframe_props(num1, num2, title) { let t0 = performance.now() // just a small sample let listE = ['dump','fullScreen','getDefaultComputedStyle','mozInnerScreenX', 'mozInnerScreenY','netscape','onmozfullscreenchange','onmozfullscreenerror'] let listU = ['BatteryManager','HID','Keyboard','Lock','Serial', 'USB','WakeLock','WebKitMutationObserver','XRAnchor','chrome','webkitCancelAnimationFrame', 'webkitMediaStream','webkitRTCPeerConnection','webkitRequestAnimationFrame', 'webkitRequestFileSystem','webkitResolveLocalFileSystemURL','webkitStorageInfo'] // FYI: don't use MIDIAccess: FF60+ behind dom.webmidi.enabled... 1752906: FF99+ default enabled if (isVer > 121) { listU = listU.filter(x => !["WakeLock"].includes(x)) } // FF122+ dom.screenwakelock.enabled: 1589554 if (isVer > 92) { listU = listU.filter(x => !["Lock"].includes(x)) } // FF93+ "dom.weblocks.enabled": 1739233: FF96+ Lock try { if (bBlock) {bB = 1} // create & append iframe let id = "iframe-window-version" let el = document.createElement("iframe") el.setAttribute("id", id) el.setAttribute('style', 'display: none') document.body.appendChild(el) // get props let iframe = document.getElementById(id) let contentWindow = iframe.contentWindow let props = Object.getOwnPropertyNames(contentWindow) // remove iframe iframe.parentNode.removeChild(iframe) props.sort() // expected let arrayE = props.filter(x => listE.includes(x)) let resE = "", boolE = false if (arrayE.length) { sDetail["expected_"+ title] = arrayE resE = mini(arrayE) + buildButton(3, "expected_"+ title, arrayE.length) if (arrayE.length > 4) {boolE = true} } else { resE = "none" } // unexpected let arrayU = props.filter(x => listU.includes(x)) // ignore BatteryManager FF72+ if (isFF && isVer < 72) {arrayU = arrayU.filter(x => !["BatteryManager"].includes(x))} let resU = "", boolU = false if (arrayU.length) { sDetail["unexpected_"+ title] = arrayU resU = mini(arrayU) + buildButton(3, "unexpected_"+ title, arrayU.length) } else { resU = "none" boolU = true } // record fnRecord(num1, title, resE, boolE, "skip", t0) fnRecord(num2, title, resU, boolU, "skip", t0) } catch(e) { fnRecord(num1, title, e.name, zErr, e.message) fnRecord(num2, title, e.name, zErr, e.message) } } function get_installtrigger(num) { // FF100+: 1754441 behind prefs slated for deprecation let strA = "[typeof] InstallTrigger", boolA = false try { let t0A = performance.now() if (bBlock) {bB = 1} let resA = typeof InstallTrigger if (resA == "object") {boolA = true} if (isVer > 99 && !boolA) { fnRecord(num, strA, zNA +": FF99 or lower" + s3 +" ["+ resA +"]"+ sc, zNA, "skip", t0A) } else { if (!isFF) {resA += " [FF99 or lower]"; boolA = zNA} fnRecord(num, strA, resA, boolA, "skip", t0A) } } catch(e) { fnRecord(num, strA, e.name, zErr, e.message) } let strB = "[window] InstallTrigger", boolB = false try { let t0B = performance.now() if (bBlock) {bB = 1} let resB = "InstallTrigger" in window if (isVer > 99 && !resB) { fnRecord(num+1, strB, zNA +": FF99 or lower" + s3 +" ["+ resB +"]"+ sc, zNA, "skip", t0B) } else { if (!isFF) {resB += " [FF99 or lower]" } fnRecord(num+1, strB, resB, resB, "skip", t0B) } } catch(e) { fnRecord(num+1, strB, e.name, zErr, e.message) } let strC = "[typeof] InstallTriggerImpl", boolC = false // FF60 or lower if (isFF && isVer < 61) { fnRecord(num+2, strC, zNA +": FF61+ required", zNA) } else { try { let t0C = performance.now() if (bBlock) {bB = 1} let resC = typeof InstallTriggerImpl if (resC == "function") {boolC = true} if (isVer > 99 && !boolC) { fnRecord(num+2, strC, zNA +": FF99 or lower" + s3 +" ["+ resC +"]"+ sc, zNA, "skip", t0C) } else { if (!isFF) {resC += " [FF99 or lower]"; boolC = zNA } fnRecord(num+2, strC, resC, boolC, "skip", t0C) } } catch(e) { fnRecord(num+2, strC, e.name, zErr, e.message) } } } function get_intl_canonical_locale(num, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} let list = ['bh','hye','no','tl','tw'], bool = false let res = [] list.forEach(function(i) { res.push(Intl.getCanonicalLocales(i)) }) let hash = res.length > 0 ? mini(res) : "none" if (hash == "df6e312d") {bool = true // FF91+ } else if (hash == "6dc73a60") {bool = true // FF70-90 } else if (hash == "0e0d831d") {bool = true // FF52-69 } // FF60-69 : bh=bh, hye=hye, no=no, tl=tl, tw=tw // FF70-90 : bh=bho, hye=hy, no=nb, tl=fil, tw=ak // FF91+ : bh=bho, hye=hy, no=no, tl=fil, tw=ak // webkit now matches // blink : bh=bh, hye=hy, no=no, tl=fil, tw=tw // webkit : bh=bh, hye=hy, no=no, tl=tl, tw=tw if (res.length > 0) { if (res.join(", ").length < 48) { hash = res.join(", ") } else { sDetail["specific_"+ title] = res hash += buildButton(3, "specific_"+ title, "details") } } fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_intl_supportedlocales(num) { // DTF, NF, RTF, LF, DN /* NOTES // FF52+ common items (130) not in blink103 common (59) = 71 items // FF52: DTF + NF / FF65: RTF / FF78: LF / FF86: DN // remove if present: bh bho, hye hy, no nb, tl fil, tw ak [aliases/maps] geckoNotBlink = [ // mix it up per test 'eo','gl','ka','mt','om', // used in C // from collator list below that is in common with this list // ^ note mt only added if .compare works 'ki','lu','qu','rn','rw', // used in PR (limited choices) 'as','fo','ln','ne','to', // used in DTF 'br','eu','ii','ks','ps', // used in DN 'ee','gd','kk','lo','yi', // used in LF 'bo','ha','lb','my','sq', // used in NF 'cy','km','mg','os','sn', // used in RTF // 35 spare 'ast','be','bm','ce','ckb','dsb','dz','ff','fur','fy', 'ga','gv','haw','hsb','ig','is','kab','kl','kok','kw', 'ky','lg','mk','mn','nd','nn','or','rm','se','sg','si', 'so','ti','ug','yo','zu' ] // webkit items not in FF105 // note: older webkit only: make sure one in each: 'co','cv','oc','nso','tig' WKnotGecko = [ 'ba','co','dv','gn','io','iu','nr','nso','nv','ny','oc','ss','st','tig','tn','ts','ve','wa' // 'cv' // cv is supported in FF109+ ] */ // COLLATOR /* geckoNotBlink = [ 'as','az','be','bo','bs','cy','dsb','dz','ee','eo','fo','ga','gl', 'ha','haw','hsb','ig','is','ka','kk','kl','km','kok','ky','lb', 'ln','lo','mk','mn','mt','my','ne','nn','om','or','pa','ps','se', 'si','sq','to','ug','uz','yi','yo','zu' ] webkit = same as gecko 91+ old_list = [ 'az','cy','dsb','ee','eo','gl','ha','haw','hsb','ig', 'ka','kk','ln','lo','mt','om','pa','se','sq','to', ] */ // PR /* FF52+ geckoNotBlink = ['ki','kok','lu','qu','rn','rw'] FF105- blinkNotGecko = ['an','dv','io','iu','lij','nr','nso','ny','ss','st','tig','tn','ts','ve','vo','wa'] FF105- webkitNotGecko = ['ba','co','cv','dv','gn','io','iu','nr','nso','nv','ny','oc','ss','st','tig','tn','ts','ve','wa'] therefore blinkOnly = ['an','lij','vo'] webkitOnly = ['ba',co','cv',''gn','ny','oc'] let list = [ 'ki','lu','qu','rn','rw', // gecko: pick 5 'ba','co','cv','gn','oc', // webkit only: pick 4 'an','lij','vo', // chrome only: not needed ] */ let oList = { 'Collator': {errVer: 50, expected: 'f7e01839', list: ['eo','gl','ka','om']}, 'DateTimeFormat': {errVer: 50, expected: '2d45bfd6', list: ['as','fo','ln','ne','to', 'co','nr','st','tig']}, 'DisplayNames': {errVer: 86, expected: '502f87c2', list: ['br','eu','ii','ks','ps','gn','iu','nv','tn']}, 'ListFormat': {errVer: 78, expected: 'a2d97917', list: ['ee','gd','kk','lo','yi','ba','io','oc','ve']}, 'NumberFormat': {errVer: 50, expected: '3709a703', list: ['bo','ha','lb','my','sq','dv','ny','oc','ts']}, 'PluralRules': {errVer: 58, expected: '73b49209', list: ['ki','lu','qu','rn','rw','ba','co','gn','oc']}, 'RelativeTimeFormat': {errVer: 65, expected: 'fc7fa0cc', list: ['cy','km','mg','os','sn','gn','nso','ss','wa']}, } let control = (['c','C']).sort(Intl.Collator('mt').compare) if (control.join('') == 'Cc') {oList['Collator']['list'].push('mt')} if (isVer > 133) { oList.DateTimeFormat.expected = 'd240302d' oList.DisplayNames.expected = 'ccf106a6' oList.ListFormat.expected = '52f07243' oList.NumberFormat.expected = '741aaeb3' oList.PluralRules.expected = '2d5fb02b' oList.RelativeTimeFormat.expected = '98ac597e' } if (isVer > 146) { // 2000225 (i think) - 'ba' (bashkir) now supported oList.ListFormat.expected = '130cf142' oList.PluralRules.expected = 'ed7c2f2a' } num = (num - 5) for (const type of Object.keys(oList)) { num += 5 let t0 = performance.now(), res = [], title = "Intl."+ type let errVer = oList[type].errVer, expected = oList[type].expected, list = oList[type].list.sort() try { if (bBlock) {bB = 1} if (type == "Collator") { res = Intl.Collator.supportedLocalesOf(list) } else if (type == "DateTimeFormat") { res = Intl.DateTimeFormat.supportedLocalesOf(list) } else if (type == "DisplayNames") { res = Intl.DisplayNames.supportedLocalesOf(list) } else if (type == "ListFormat") { res = Intl.ListFormat.supportedLocalesOf(list) } else if (type == "NumberFormat") { res = Intl.NumberFormat.supportedLocalesOf(list) } else if (type == "PluralRules") { res = Intl.PluralRules.supportedLocalesOf(list) } else if (type == "RelativeTimeFormat") { res = Intl.RelativeTimeFormat.supportedLocalesOf(list) } let hash = res.length > 0 ? mini(res) : "none" //console.log(title, hash) let bool = hash == expected if (res.length > 0) { if (res.join(", ").length < 48) { hash = res.join(", ") } else { sDetail["specific_"+ title] = res hash += buildButton(3, "specific_"+ title, res.length) } } fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { if (isFF) { if (isVer >= errVer) { fnRecord(num, title, e.name, zErr, e.message) } else { let msg = e.message msg = msg.replace("can't access property \"supportedLocalesOf\", ", "") // trim *error_fix if (e.name == "TypeError" && msg == "Intl." + type +" is undefined") { fnRecord(num, title, zNA +": FF" + errVer +"+ required", zNA) } else { fnRecord(num, title, e.name, zErr, e.message) } } } else { fnRecord(num, title, zNA +": not supported", zNA) } } } } function get_intl_supportedvaluesof(num, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} let aSupported = Intl.supportedValuesOf("timeZone") let aExpected = ['CET','EET','EST','HST','MET','MST','WET',] // b7a58624 let res = aSupported.filter(x => aExpected.includes(x)) let hash = res.length > 0 ? mini(res) : "none" let bool = hash == "b7a58624" if (res.length > 0) { if (res.join(", ").length < 48) { hash = res.join(", ") } else { sDetail["specific_"+ title] = res hash += buildButton(3, "specific_"+ title, res.length) } } if (isVer > 134) { bool = zNA hash = zNA +": FF109 or lower "+ s3 +"["+ hash +"]"+ sc } else if (!isFF) { bool = zNA hash += " [FF109 or lower]" } fnRecord(num, title, hash, bool, "skip", t0) //fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { // FF92- not supported if (isVer < 93) { fnRecord(num, title, zNA +": not supported", zNA) } else { fnRecord(num, title, e.name, zErr, e.message) } } } function get_js_client_hints(num, title) { // testing: https://web.dev/user-agent-client-hints/ // https://user-agent-client-hints.glitch.me/javascript.html // TypeError can't access property "X", navigator.X is undefined function output() { if (res.length > 0) { res.sort() sDetail["unexpected_"+ title] = res let hash = mini(res) + buildButton(3, "unexpected_"+ title , res.length) fnRecord(num, title, hash, false, "skip", t0) } else { fnRecord(num, title, "none", true, "skip", t0) } } let res = [] let t0 = performance.now() try { let resB = navigator.userAgentData.brands resB.forEach(function(object) { let valueB = fnClean(object.version) res.push("brands: "+ object.brand +": "+ valueB) }) } catch(e) {} try { res.push("mobile: " + navigator.userAgentData.mobile) } catch(e) {} try { res.push("platform: " + navigator.userAgentData.platform) } catch(e) {} try { navigator .userAgentData.getHighEntropyValues( ["architecture","bitness","brands","mobile","model","platform","platformVersion","uaFullVersion"] ).then(ua => { const names = Object.keys(ua) for (const k of names) { let valueU = fnClean(ua[k]) res.push("high entropy: "+ k +": "+ valueU) } output() }) } catch(e) { output() } } function get_last_prototype_keys(num1, title) { // HTMLAnchorElement let title1 = title +" HTMLAnchorElement" try { let t0 = performance.now() if (bBlock) {bB = 1} Object.keys(HTMLAnchorElement.prototype) let props1 = Object.getOwnPropertyNames(HTMLAnchorElement.prototype) // includes the constructor let res1 = props1.slice(-3).join(", ") // search, hash, constructor fnRecord(num1, title1, res1, res1 == "search, hash, constructor", "skip", t0) } catch(e) { fnRecord(num1, title1, e.name, zErr, e.message) } // HTMLLinkElement let title2 = title +" HTMLLinkElement" let num2 = num1 + 1 try { let t0 = performance.now() if (bBlock) {bB = 1} Object.keys(HTMLLinkElement.prototype) let props2 = Object.getOwnPropertyNames(HTMLLinkElement.prototype) // includes the constructor let res2 = props2.slice(-3).join(", ") let bool2 = res2 == "as, sheet, constructor" // 52-55: integrity, sheet, constructor if (isVer <= 56 && res2 == "integrity, sheet, constructor") {bool2 = true} fnRecord(num2, title2, res2, bool2, "skip", t0) } catch(e) { fnRecord(num2, title2, e.name, zErr, e.message) } } function get_locale_compare(num, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} let list = [ ["cy","n","ng"], ["gl","\u00F1","ng"], ["ha","ts","tt"], ["hsb","\u0107","\u0109"], ["ig","c","ch"], ["ka","\u0107","\u10D0"], ["mt","c","C"], // MAC: Intl.Collator supportedlocale but does not collate it ["om","ch","\u00ED"], ["sq","\u00EB","ez"], ["to","\u00ED","\u00EE"], ] let res = [] list.forEach(function(item) { res.push(item[1].localeCompare(item[2], item[0])) }) let hash = res.length > 0 ? mini(res) : "none" let bool = hash == "2e086db5" if (res.length > 0) { hash = res.join(", ") } fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_math(num, title) { // polyfill function cbrt(x) { try { let y = Math.pow(Math.abs(x), 1 / 3) return x < 0 ? -y : y } catch(e) { return "error" } } try { let t0 = performance.now() if (bBlock) {bB = 1} let res = [] for(let i=0; i < 6; i++) { try { let fnResult = "unknown" if (i == 0) {fnResult = cbrt(Math.PI) // polyfill } else if (i == 1) {fnResult = Math.log10(7*Math.LOG10E) } else if (i == 2) {fnResult = Math.log10(2*Math.SQRT1_2) } else if (i == 3) {fnResult = Math.acos(0.123) } else if (i == 4) {fnResult = Math.acosh(Math.SQRT2) } else if (i == 5) {fnResult = Math.atan(2) } res.push(fnResult) } catch(e) { res.push("error") } } // I have to use sha1 due to legacy data let hash = sha1(res.join()).substring(0,20) let engine = "unkown" if (hash == "ede9ca53efbb1902cc21") {engine = "blink" } else if (hash == "05513f36d87dd78af60a") {engine = "webkit" } else if (hash == "38172d9426d77af71baa") {engine = "edgeHTML" } else if (hash == "36f067c652c8cfd90725") {engine = "trident" } else if (hash == "225f4a612fdca4065043") {engine = "gecko" } else if (hash == "cb89002a8d6fabf859f6") {engine = "gecko" } let bool = engine == "gecko" ? true : false // now redo as mini sDetail["expected_"+ title] = res hash = mini(res) + buildButton(3, "expected_"+ title, engine) fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_media_constraints(num, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} let noiseFF = [ 'groupId', // FF70+ 'channelCount', // FF56+ 'autoGainControl','noiseSuppression', // FF55+ 'mozAutoGainControl','mozNoiseSuppression', // FF54- 'resizeMode', // FF144+ ] let data = navigator.mediaDevices.getSupportedConstraints() let res = Object.keys(data) res = res.filter(x => !noiseFF.includes(x)) res.sort() let hash = res.length > 0 ? mini(res) : "none" let bool = hash == "27fb15ca" // FF52+ if (res.length > 0) { sDetail["expected_"+ title] = res hash += buildButton(3, "expected_"+ title, res.length) } fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { // e.g. TB disables mediaDevices let error = mini(e.name +": "+ e.message), isNA = false if (error == "f2244861") { // TypeError: can't access property "getSupportedConstraints", navigator.mediaDevices is undefined isNA = true } else if (error == "9fc627cf") { // TypeError: navigator.mediaDevices is undefined isNA = true } if (isNA) { fnRecord(num, title, zNA +": disabled", zNA) } else { fnRecord(num, title, e.name, zErr, e.message) } } } function get_mimetypes(num, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} if ("mimeTypes" in navigator) { let res = [], bool = false let m = navigator.mimeTypes if (m.length == 0) { fnRecord(num, title, "none", true, "skip", t0) } else { for (let i=0; i < m.length; i++) { res.push( m[i].type + (m[i].description == "" ? ": * " : ": "+ m[i].type) + (m[i].suffixes == "" ? ": *" : ": "+ m[i].suffixes) ) } res.sort() // FF84 or lower allow just Flash if (isFF && isVer < 85 && res.length == 2) { let mime1 = res[0].split(":")[0] let mime2 = res[1].split(":")[0] if (mime1 == "application/x-futuresplash" && mime2 == "application/x-shockwave-flash") { bool = true } } let hash = mini(res) if (isVer > 98 || !isFF) { // allow non-FF to have the same // FF99+: controlled by pdfjs.disabled // 1720353: hardcoded mimeTypes if (hash == "d63af80f") {bool = true} } sDetail["expected_"+ title] = res hash += buildButton(3, "expected_"+ title, res.length) fnRecord(num, title, hash, bool, "skip", t0) } } else { fnRecord(num, title, e.name, zErr) } } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_moz_colors(num, title) { let aList = [ // css4 '-moz-activehyperlinktext','-moz-default-color','-moz-default-background-color','-moz-hyperlinktext','-moz-visitedhyperlinktext', // stand-ins '-moz-buttondefault','-moz-buttonhoverface','-moz-buttonhovertext','-moz-cellhighlight','-moz-cellhighlighttext','-moz-combobox','-moz-comboboxtext','-moz-dialog','-moz-dialogtext','-moz-dragtargetzone','-moz-eventreerow','-moz-field','-moz-fieldtext','-moz-html-cellhighlight','-moz-html-cellhighlighttext','-moz-mac-chrome-active','-moz-mac-chrome-inactive','-moz-mac-disabledtoolbartext','-moz-mac-focusring','-moz-mac-menuselect','-moz-mac-menushadow','-moz-mac-menutextdisable','-moz-mac-menutextselect','-moz-mac-secondaryhighlight','-moz-menubarhovertext','-moz-menubartext','-moz-menuhover','-moz-menuhovertext','-moz-nativehyperlinktext','-moz-oddtreerow','-moz-win-communicationstext','-moz-win-mediatext', // moz '-moz-accent-color','-moz-accent-color-foreground','-moz-appearance','-moz-colheaderhovertext','-moz-colheadertext','-moz-gtk-buttonactivetext','-moz-gtk-info-bar-text','-moz-mac-accentdarkestshadow','-moz-mac-accentdarkshadow','-moz-mac-accentface','-moz-mac-accentlightesthighlight','-moz-mac-accentlightshadow','-moz-mac-accentregularhighlight','-moz-mac-accentregularshadow','-moz-mac-active-menuitem','-moz-mac-active-source-list-selection','-moz-mac-buttonactivetext','-moz-mac-defaultbuttontext','-moz-mac-menuitem','-moz-mac-menupopup','-moz-mac-source-list','-moz-mac-source-list-selection','-moz-mac-tooltip','-moz-mac-vibrancy-dark','-moz-mac-vibrancy-light','-moz-mac-vibrant-titlebar-dark','-moz-mac-vibrant-titlebar-light','-moz-win-accentcolor','-moz-win-accentcolortext','-moz-win-communications-toolbox','-moz-win-media-toolbox', ] aList.sort() try { let t0 = performance.now() if (bBlock) {bB = 1} let aRes = [] let element = dom.mozColor let strColor = "rgba(1, 2, 3, 0.5)" aList.forEach(function(style) { element.style.backgroundColor = strColor // always reset element.style.backgroundColor = style let rgb = window.getComputedStyle(element, null).getPropertyValue("background-color") if (rgb !== strColor) { aRes.push(style +":"+ rgb) // only record those affected } }) let bool = aRes.length > 0 if (bool) { sDetail["expected_"+ title] = aRes } let hash = bool ? mini(aRes) + buildButton(3, "expected_"+ title, aRes.length) : "none" fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_moz_fonts(num, title) { let aResults = [], m = "-moz-", aFonts = [m+"window",m+"desktop",m+"document",m+"workspace",m+"info",m+"pull-down-menu",m+"dialog",m+"button",m+"list",m+"field"] try { let t0 = performance.now() let el = dom.mozFont if (bBlock) {bB = 1} aFonts.forEach(function(font){ // catch blocked let test = getComputedStyle(el).getPropertyValue("font-family") el.style.font = "99px sans-serif" try {el.style.font = font} catch(err) {} let s = "" if (window.getComputedStyle) { try { s = getComputedStyle(el, null) } catch(e) {} } if (s !== "") { let f = s.fontSize != "99px" ? s.fontFamily : undefined if (f !== undefined) {aResults.push(f)} } }) let bool = aResults.length > 0 if (bool) { sDetail["expected_"+ title] = aResults } let hash = bool ? mini(aResults) + buildButton(3, "expected_"+ title, aResults.length) : "none" if (isVer > 108) { bool = zNA; hash = zNA +": FF108 or lower" // 1802957 } if (!isFF) {hash += " [FF108 or lower]"; bool = zNA} fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_moz_objects(num, name) { let title = "["+ name +"] moz" try { let t0 = performance.now() if (bBlock) {bB = 1} let res = [] let oList = {} let isObj = false let useHas = false // use hasOwnProperty or default getOwnPropertyDescriptor if (name === "Document" && "function" === typeof Document) { oList[name] = [Document.prototype, ["mozSetImageElement","mozCancelFullScreen","mozFullScreen","mozFullScreenEnabled", "mozFullScreenElement","onmozfullscreenchange","onmozfullscreenerror"] ] } if (name === "HTMLElement" && "function" === typeof HTMLElement) { oList[name] = [HTMLElement.prototype, ["onmozfullscreenchange","onmozfullscreenerror"]] } if (name === "HTMLMediaElement" && "function" === typeof HTMLMediaElement) { oList[name] = [HTMLMediaElement.prototype, ["mozCaptureStream","mozCaptureStreamUntilEnded", "mozGetMetadata","mozPreservesPitch","mozAudioCaptured","mozFragmentEnd"] ] } if (name === "HTMLVideoElement" && "function" === typeof HTMLVideoElement) { oList[name] = [HTMLVideoElement.prototype, ["mozParsedFrames","mozDecodedFrames", "mozPresentedFrames","mozPaintedFrames","mozFrameDelay","mozHasAudio"] ] } if (name === "MouseEvent" && "function" === typeof MouseEvent) { oList[name] = [MouseEvent.prototype, ["MOZ_SOURCE_UNKNOWN","MOZ_SOURCE_MOUSE","MOZ_SOURCE_PEN","MOZ_SOURCE_ERASER", "MOZ_SOURCE_CURSOR","MOZ_SOURCE_TOUCH","MOZ_SOURCE_KEYBOARD","mozPressure", "mozInputSource","MOZ_SOURCE_UNKNOWN","MOZ_SOURCE_MOUSE","MOZ_SOURCE_PEN", "MOZ_SOURCE_ERASER","MOZ_SOURCE_CURSOR","MOZ_SOURCE_TOUCH","MOZ_SOURCE_KEYBOARD"] ] } if (name === "Screen" && "function" === typeof Screen) { oList[name] = [Screen.prototype, ["mozLockOrientation","mozUnlockOrientation","mozOrientation","onmozorientationchange"] ] } if (name === "SVGElement" && "function" === typeof SVGElement) { oList[name] = [SVGElement.prototype,["onmozfullscreenchange","onmozfullscreenerror"]] } if (name === "HTMLInputElement" && "function" === typeof HTMLInputElement) { oList[name] = [HTMLInputElement.prototype,["mozIsTextField"]] } if (name === "Navigator" && "function" === typeof Navigator) { oList[name] = [Navigator.prototype,["mozGetUserMedia"]] } if (name === "HTMLCanvasElement" && "function" === typeof HTMLCanvasElement) { oList[name] = [HTMLCanvasElement.prototype,["mozOpaque","mozPrintCallback"]] } if (name === "CanvasRenderingContext2D" && "function" === typeof CanvasRenderingContext2D) { oList[name] = [CanvasRenderingContext2D.prototype, ["mozCurrentTransform","mozCurrentTransformInverse","mozTextStyle","mozImageSmoothingEnabled"] ] } if (name === "MathMLElement" && "function" === typeof MathMLElement) { oList[name] = [MathMLElement.prototype,["onmozfullscreenchange","onmozfullscreenerror"]] } if (name === "DataTransfer" && "function" === typeof DataTransfer) { oList[name] = [DataTransfer.prototype,["mozCursor","mozUserCancelled","mozSourceNode"]] } if (name === "ShadowRoot" && "function" === typeof ShadowRoot) { oList[name] = [ShadowRoot.prototype,["mozFullScreenElement"]] } if (name === "XMLHttpRequest" && "function" === typeof XMLHttpRequest) { oList[name] = [XMLHttpRequest.prototype,["mozAnon","mozSystem"]] } if (name === "IDBObjectStore" && "function" === typeof IDBObjectStore) { oList[name] = [IDBObjectStore.prototype,["mozGetAll"]] } if (name === "IDBIndex" && "function" === typeof IDBIndex) { oList[name] = [IDBIndex.prototype,["mozGetAll","mozGetAllKeys"]] } if (name === "OfflineResourceList" && "function" === typeof OfflineResourceList) { oList[name] = [OfflineResourceList.prototype, ["mozHasItem","mozItem","mozAdd","mozRemove","mozItems","mozLength"] ] } if (name === "Element" && "function" === typeof Element) { useHas = true oList[name] = [Element.prototype,["mozMatchesSelector","​mozRequestFullScreen"]] } if (name === "CSS2|CSSStyle") { let source if ("function" === typeof CSS2Properties) { source = CSS2Properties title = "[CSS2Properties] moz" } else if ("function" === typeof CSSStyleProperties) { source = CSSStyleProperties title = "[CSSStyleProperties] moz" } if (undefined !== source) { oList[name] = [source.prototype, ['MozAnimation','MozAnimationDelay','MozAnimationDirection','MozAnimationDuration','MozAnimationFillMode', 'MozAnimationIterationCount','MozAnimationName','MozAnimationPlayState','MozAnimationTimingFunction', 'MozAppearance','MozBackfaceVisibility','MozBorderEnd','MozBorderEndColor','MozBorderEndStyle', 'MozBorderEndWidth','MozBorderImage','MozBorderStart','MozBorderStartColor','MozBorderStartStyle', 'MozBorderStartWidth','MozBoxAlign','MozBoxDirection','MozBoxFlex','MozBoxOrdinalGroup','MozBoxOrient', 'MozBoxPack','MozBoxSizing','MozFloatEdge','MozFontFeatureSettings','MozFontLanguageOverride', 'MozForceBrokenImageIcon','MozHyphens','MozImageRegion','MozMarginEnd','MozMarginStart','MozOrient', 'MozPaddingEnd','MozPaddingStart','MozPerspective','MozPerspectiveOrigin','MozTabSize','MozTextSizeAdjust', 'MozTransform','MozTransformOrigin','MozTransformStyle','MozTransition','MozTransitionDelay', 'MozTransitionDuration','MozTransitionProperty','MozTransitionTimingFunction','MozUserFocus','MozUserInput', 'MozUserModify','MozUserSelect','MozWindowDragging', // aliases '-moz-animation','-moz-animation-delay','-moz-animation-direction','-moz-animation-duration', '-moz-animation-fill-mode','-moz-animation-iteration-count','-moz-animation-name','-moz-animation-play-state', '-moz-animation-timing-function','-moz-appearance','-moz-backface-visibility','-moz-border-end', '-moz-border-end-color','-moz-border-end-style','-moz-border-end-width','-moz-border-image','-moz-border-start', '-moz-border-start-color','-moz-border-start-style','-moz-border-start-width','-moz-box-align', '-moz-box-direction','-moz-box-flex','-moz-box-ordinal-group','-moz-box-orient','-moz-box-pack', '-moz-box-sizing','-moz-float-edge','-moz-font-feature-settings','-moz-font-language-override', '-moz-force-broken-image-icon','-moz-hyphens','-moz-image-region','-moz-margin-end','-moz-margin-start', '-moz-orient','-moz-padding-end','-moz-padding-start','-moz-perspective','-moz-perspective-origin', '-moz-tab-size','-moz-text-size-adjust','-moz-transform','-moz-transform-origin','-moz-transform-style', '-moz-transition','-moz-transition-delay','-moz-transition-duration','-moz-transition-property', '-moz-transition-timing-function','-moz-user-focus','-moz-user-input','-moz-user-modify','-moz-user-select', '-moz-window-dragging', ] ] } } // object: don't use, we can do this elsewhere if (name === "window" && "object" == typeof window) { isObj = true oList[name] = [window,["mozRTCPeerConnection","CSSMozDocumentRule"]] } // do it! if (oList[name] !== undefined) { let obj = oList[name][0] let props = oList[name][1] if (isObj) { props.forEach(function(element) { if (element in obj) {res.push(element)} }) } else if (useHas) { props.forEach(function(element) { if (obj.hasOwnProperty(element)) {res.push(element)} }) } else { props.forEach(function(element) { if ("object" === typeof Object.getOwnPropertyDescriptor(obj, element)) { res.push(element) } }) } res.sort() } let hash = "none", bool = false if (res.length > 0) { if (res.join(", ").length < 48) { hash = res.join(", ") } else { sDetail["expected_"+ title] = res hash = mini(res) + buildButton(3, "expected_"+ title, res.length) } bool = true } else if (isFF) { // FF none exceptions if (isVer < 63 && name === "ShadowRoot") {bool = zNA, hash = zNA +": FF63+ required"} if (isVer < 71 && name === "MathMLElement") {bool = zNA, hash = zNA +": FF71+ required"} if (isVer > 141 && name === 'HTMLInputElement') {bool = zNA, hash = zNA +": FF141 or lower"} if (name === "OfflineResourceList") {bool = zNA, hash = zNA +": assuming Beta/Dev/Nightly pref"} if (name === "Navigator") { // TB/PM build without WebRTC: mozGetUserMedia // and because of that also disable gUM (or code on sites suffer) if (undefined == window.RTCPeerConnection) { bool = zNA hash = zNA +": --disable-webrtc"+ s3 +" ["+ hash +"]"+ sc } else if (isFF) { // behind prefs: both media.peerconnection.enabled + media.navigator.enabled bool = zNA hash = zNA + s3 +" ["+ hash +"]"+ sc + sb +" [not firefox default]"+ sc } } if (isVer > 112 && name === "CanvasRenderingContext2D") { // 1822955 bool = zNA hash = zNA +": FF112 or lower" } } else { // non-FF if (name === "CanvasRenderingContext2D") { bool = zNA hash += " [FF112 or lower]" } } fnRecord(num, title, hash, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_nav_keys(num1, num2, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} let aNavKeys = Object.keys(Object.getOwnPropertyDescriptors(Navigator.prototype)) // expected FF only let aExpected = ["buildID","oscpu","taintEnabled"] let resE = aNavKeys.filter(x => aExpected.includes(x)) let boolE = resE.length == 3 ? true : false resE = resE.length > 0 ? resE.join(", ") : "none" fnRecord(num1, title, resE, boolE, "skip", t0) // not expected: blink items let aNot = ["canShare","clearAppBadge","deviceMemory","getBattery","getInstalledRelatedApps", "getUserMedia","globalPrivacyControl","hid","keyboard","locks","managed","presentation","scheduling", "serial","setAppBadge","unregisterProtocolHandler","usb","userActivation","userAgentData","wakeLock", "webkitGetUserMedia","webkitPersistentStorage","webkitTemporaryStorage","xr","SharedWorker","Worker"] // FYI: don't use requestMIDIAccess: FF60+ behind dom.webmidi.enabled... 1752906: FF99+ default enabled if (isVer > 121) { aNot = aNot.filter(x => !["wakeLock"].includes(x)) } // FF122+ dom.screenwakelock.enabled 1589554 if (isVer > 119) { aNot = aNot.filter(x => !["userActivation"].includes(x)) } // 1791079: FF120+ if (isVer > 95) {aNot = aNot.filter(x => !["canShare"].includes(x)) } // 1666203: FF96+ canShare if (isVer > 94 || isEngine == "goanna") { aNot = aNot.filter(x => !["globalPrivacyControl"].includes(x))} // 1670058: FF95+ globalPrivacyControl | PM 30 if (isVer > 92) { aNot = aNot.filter(x => !["locks"].includes(x)) } // FF93+ dom.weblocks.enabled: 1739233: FF96+ locks let resN = aNavKeys.filter(x => aNot.includes(x)) let boolN = resN.length == 0 if (resN.length > 0) { sDetail["unexpected_"+ title] = resN resN = mini(resN) + buildButton(3, "unexpected_"+ title, resN.length) } else { resN = "none" } fnRecord(num2, title, resN, boolN, "skip", t0) } catch(e) { fnRecord(num1, title, e.name, zErr, e.message) fnRecord(num2, title, e.name, zErr, e.message) } } function get_nav_screen(num) { let strA = "[left] screen", boolA = false try { let t0A = performance.now() if (bBlock) {bB = 1} let resA = screen.left if (!isNaN(resA)) {boolA = true} else {resA = fnClean(resA)} fnRecord(num, strA, resA, boolA, "skip", t0A) } catch(e) { fnRecord(num, strA, e.name, zErr, e.message) } let strB = "[top] screen", boolB = false try { let t0B = performance.now() if (bBlock) {bB = 1} let resB = screen.top if (!isNaN(resB)) {boolB = true} else {resB = fnClean(resB)} fnRecord(num+1, strB, resB, boolB, "skip", t0B) } catch(e) { fnRecord(num+1, strB, e.name, zErr, e.message) } } function get_nav_values(num1, num2, num3) { // expected let strA = "[oscpu] navigator", boolA = false try { let t0A = performance.now() if (bBlock) {bB = 1} let resA = navigator.oscpu if (typeof resA !== "string") { //console.debug(typeof resA) } else { if (resA == "") {resA = "empty string" } else if (resA == undefined) {resA = zU } else if (resA == zU) {resA = zUQ } else {boolA = true} } fnRecord(num1, strA, resA, boolA, "skip", t0A) } catch(e) { fnRecord(num1, strA, e.name, zErr, e.message) } // not expected // bluetooth let strBT = "[bluetooth] navigator", boolBT = false, numBT = num2 try { let t0BT = performance.now() if (bBlock) {bB = 1} let resBT = navigator.bluetooth if (resBT === undefined) { fnRecord(numBT, strBT, zU, true, "skip", t0BT) } else { try { navigator.bluetooth.getAvailability().then(available => { if (available) { fnRecord(numBT, strBT, "supported", boolBT) } else { fnRecord(numBT, strBT, "not supported", boolBT) } }) } catch(e) { fnRecord(numBT, strBT, e.name, zErr) } } } catch(e) { fnRecord(numBT, strBT, e.name, zErr, e.message) } // deviceMemory let strDM = "[deviceMemory] navigator", boolDM = false, numDM = num2+2 try { let t0DM = performance.now() if (bBlock) {bB = 1} let resDM = navigator.deviceMemory if (resDM == undefined) {boolDM = true} resDM = fnClean(resDM) fnRecord(numDM, strDM, resDM, boolDM, "skip", t0DM) } catch(e) { fnRecord(numDM, strDM, e.name, zErr, e.message) } // globalprivacycontrol let strG = "[gpc] navigator", boolG = false, numG = num2+6 try { let t0G = performance.now() if (bBlock) {bB = 1} let resG = navigator.globalPrivacyControl resG = fnClean(resG) if (resG == "undefined") {boolG = true } else if (isEngine == "goanna" && resG == "empty string") {boolG = true; resG += s3 + " [goanna]"+ sc} // 1670058: FF95+ globalPrivacyControl if (isVer > 94) { resG = zNA +": FF94 or lower" + s3 +" ["+ resG +"]" + sc boolG = zNA } else if (!isFF) { resG += " [FF94 or lower]" boolG = zNA } fnRecord(numG, strG, resG, boolG, "skip", t0G) } catch(e) { fnRecord(numG, strG, e.name, zErr, e.message) } // keyboard let strK = "[keyboard] navigator", numK = num2+8 let t0K = performance.now() try { if (bBlock) {bB = 1} let resK = navigator.keyboard if (resK == undefined) { fnRecord(numK, strK, "undefined", true, "skip", t0K) } else { let keys = [] // https://wicg.github.io/keyboard-map/ // https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system let listK = ['Backquote','Backslash','Backspace','BracketLeft','BracketRight','Comma', 'Digit0','Digit1','Digit2','Digit3','Digit4','Digit5','Digit6','Digit7','Digit8','Digit9', 'Equal','IntlBackslash','IntlRo','IntlYen','KeyA','KeyB','KeyC','KeyD','KeyE','KeyF','KeyG', 'KeyH','KeyI','KeyJ','KeyK','KeyL','KeyM','KeyN','KeyO','KeyP','KeyQ','KeyR','KeyS','KeyT', 'KeyU','KeyV','KeyW','KeyX','KeyY','KeyZ','Minus','Period','Quote','Semicolon','Slash'] resK.getLayoutMap().then(keyboardLayoutMap => { listK.forEach(function(key) { try {keys.push(key +": "+ keyboardLayoutMap.get(key))} catch(e) {keys.push(key +": e.name")} }) sDetail["unexpected_"+ strK] = keys let hash = mini(keys) + buildButton(3, "unexpected_"+ strK , keys.length) fnRecord(numK, strK, hash, false, "skip", t0K) }) } } catch(e) { fnRecord(numK, strK, e.name, zErr, e.message) } // vendor let strV = "[vendor] navigator", boolV = false, numV = num2+10 try { let t0V = performance.now() if (bBlock) {bB = 1} let resV = navigator.vendor if (resV == "") {boolV = true} resV = fnClean(resV) fnRecord(numV, strV, resV, boolV, "skip", t0V) } catch(e) { fnRecord(numV, strV, e.name, zErr, e.message) } // specific let strX = "[buildID] navigator", boolX = false try { let t0X = performance.now() if (bBlock) {bB = 1} let resX = navigator.buildID if (isVer > 63) { // all FF64+ if (resX == "20181001000000") {boolX = true} } else { resX = fnClean(resX) // FF52-63: 14 digit number starting with 2017 let year = resX.slice(0,4) *1 if (resX.length == 14) { let isCheck = false if (year > 2016 && year < 2020) {isCheck = true} if (isVer == 52) {if (year > 2016) {isCheck = true}} // FF52 waterfox basilisk palemoon if (isCheck) { if (!isNaN(resX * 1)) {boolX = true} } } else { // RFP FF56-63 = 20100101 // note: not worth checking if RFP is enabled based on old-timey settings if (resX == "20100101") {boolX = true} } } fnRecord(num3, strX, resX, boolX, "skip", t0X) } catch(e) { fnRecord(num3, strX, e.name, zErr, e.message) } // DNT: FF: 1, unspecified let strDNT = "[doNotTrack] navigator", boolDNT = false, numDNT = num3+2 let t0DNT = performance.now() try { if (bBlock) {bB = 1} let testDNT = navigator.doNotTrack if (testDNT == 1 || testDNT == "unspecified") { boolDNT = true } else { testDNT = fnClean(testDNT) } if (isFF && !boolDNT) { // PM not supported: returns undefined if (isEngine == "goanna" && testDNT == "undefined") { fnRecord(numDNT, strDNT, testDNT, true, "skip", t0DNT) } else { fnRecord(numDNT, strDNT, testDNT, zErr, "skip", t0DNT) } } else { fnRecord(numDNT, strDNT, testDNT, boolDNT, "skip", t0DNT) } } catch(e) { fnRecord(numDNT, strDNT, e.name, zErr, e.message) } let strY = "[productSub] navigator", boolY = false, numY = num3+4 try { let t0Y = performance.now() if (bBlock) {bB = 1} let resY = navigator.productSub if (resY == "20100101") {boolY = true} else {resY = fnClean(resY)} fnRecord(numY, strY, resY, boolY, "skip", t0Y) } catch(e) { fnRecord(numY, strY, e.name, zErr, e.message) } // not expected // getbattery let strB = "[getBattery] navigator", boolB = false, numB = num2+4 let resB = [] let t0B = performance.now() try { if (bBlock) {bB = 1} // not going to add eventlisteners navigator.getBattery().then(function(battery) { try {resB.push(battery.level * 100 + "%")} catch(e) {resB.push("error")} try {resB.push((battery.charging ? "": "not ") +"charging")} catch(e) {resB.push("error")} try {resB.push(battery.chargingTime)} catch(e) {resB.push("error")} try {resB.push(battery.dischargingTime)} catch(e) {resB.push("error")} fnRecord(numB, strB, resB.join(", "), false, "skip", t0B) }) } catch(e) { if (e.name == "TypeError" && e.message.substring(0,38) == "navigator.getBattery is not a function") { if (e.message == "navigator.getBattery is not a function") {boolB = true} // webkit wil be false fnRecord(numB, strB, e.name, boolB, e.message, t0B) } else { fnRecord(numB, strB, e.name, zErr, e.message) } } } function get_obj_enumeration(num, title) { // 1762188 try { let t0 = performance.now() if (bBlock) {bB = 1} let t = ['a','b'], result = [] for (let i in t) { for (let j in t) { result.push([i, j, t[i], t[j]]) let v = t[i] delete t[i] t[i] = v } } let res = result.join(",") // nonFF = 0,0,a,a,0,1,a,b,1,0,b,a,1,1,b,b let bool = res == "0,0,a,a,0,1,a,b,1,0,b,a" ? true : false fnRecord(num, title, res, bool, "skip", t0) } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_permission(num, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} let userVis = "userVisibleOnly" // non-FF navigator.permissions.query({name:"push"}).then(function(result) { let res = result.state let bool = (res == "prompt" || res == "denied" || res == "granted") ? true : false fnRecord(num, title, res, bool, "skip", t0) }).catch(error => { if ((error.message).includes(userVis)) { fnRecord(num, title, userVis, false, "skip", t0) } else { fnRecord(num, title, error.name, zErr, error.message) } }) } catch(e) { // not supported in webkit: https://caniuse.com/?search=push%20permission if (isEngine == "webkit") { fnRecord(num, title, "not supported", false) } else { fnRecord(num, title, e.name, zErr, e.message) } } } function get_plugins(num, title) { try { let t0 = performance.now() if (bBlock) {bB = 1} if ("plugins" in navigator) { let res = [], bool = false let p = navigator.plugins if (p.length == 0) { fnRecord(num, title, "none", true, "skip", t0) } else { for (let i=0; i < p.length; i++) { res.push(p[i].name + (p[i].filename == "" ? ": * " : ": "+ p[i].filename) + (p[i].description == "" ? ": *" : ": "+ p[i].description)) } res.sort() // FF84 or lower allow just Flash if (isFF && isVer < 85 && res.length == 1) { if (res[0].split(":")[0] == "Shockwave Flash") {bool = true} } let hash = mini(res) if (isVer > 98 || !isFF) { // allow non-FF to have the same // FF99+: controlled by pdfjs.disabled // 1720353: hardcoded mimeTypes if (hash == "b46e6634") { bool = true } } sDetail["expected_"+ title] = res hash += buildButton(3, "expected_"+ title, res.length) fnRecord(num, title, hash, bool, "skip", t0) } } else { fnRecord(num, title, e.name, zErr) } } catch(e) { fnRecord(num, title, e.name, zErr, e.message) } } function get_stacklength(num1, num2, num3) { let t0 = performance.now() let level = 0, test1 = 0 function recurse() { level++ recurse() } try { recurse() } catch(e) { test1 = level } level = 0 try { recurse() } catch(e) { // timing an error property lookup is not the same as generating a alow recurse one // columnNumber let strCN = "[columnNumber] error", errCN = false try { let t0E = performance.now() if (bBlock) {bB = 1} let resCN = e.columnNumber let testCN = resCN == undefined? false : true fnRecord(num1, strCN, resCN, testCN, "skip", t0E) } catch(n) { fnRecord(num1, strCN, n.name, zErr, n.message) } // fileName let strFN = "[fileName] error", errFN = false try { let t0F = performance.now() if (bBlock) {bB = 1} let resFN = e.fileName if (resFN !== undefined) {resFN = resFN.slice(0,8) + "..."} let testFN = resFN == undefined? false : true fnRecord(num1+1, strFN, resFN, testFN, "skip", t0F) } catch(n) { fnRecord(num1+1, strFN, n.name, zErr, n.message) } // lineNumber let strLN = "[lineNumber] error", errLN = false try { let t0LN = performance.now() if (bBlock) {bB = 1} let resLN = e.lineNumber let testLN = resLN == undefined? false : true fnRecord(num1+2, strLN, resLN, testLN, "skip", t0LN) } catch(n) { fnRecord(num1+2, strLN, n.name, zErr, n.message) } // first error let perf = (t0- performance.now()) + performance.now() let strRE = "error", resRE = e.name +": "+ e.message if (resRE == "InternalError: too much recursion") { fnRecord(num3, strRE, resRE, true, "skip", perf) } else { fnRecord(num3, strRE, resRE, false, "skip", ) } // stack length: only for FYI purposes let strSL = "stack length" try { if (bBlock) {bB = 1} let resSL = e.stack.toString().length resSL = zNA +": FYI: "+ s3 +"["+ resSL +"]"+sc fnRecord(num2, strSL, resSL, zNA, "skip", perf) } catch(n) { fnRecord(num2, strSL, n.name, zErr, n.message, perf) } } } function get_storage_estimate(num, title) { // Notes: Win10 VM is 52gb / 33gb spare | FF is always the same in PB mode // 2147483648 : FF57-96 Windows / Android / Win 10 VM FF60-96 /* FF97+ is not stable enough: see https://bugzilla.mozilla.org/show_bug.cgi?id=1735713 10737418240 : Windows, vannTenn's + bashonly's Linux, Android Fabrizio 100gb spare from 128gb 5641604300 : Android Fabrizio 49gb spare from 64gb 5512729395 : Android Thorin 44gb spare from 64gb 5301081292 : Android bashonly 40gb spare from 64gb 5256596684 : Win 10 VM 33gb spare from 52gb 2934867968 : Debian XCFE 2glops 650gb spare from 1TB 1521166745 : Ubuntu VM Fabrizio with 15GB of storage 1177328025 : Android aleyvo 1.5gb spare from 16gb */ // other: who cares if they match // brave: 2147483648 (same in incognito and Tor window) // opera: 310418104 normal // opera: 521917312 private // chrome: 1200238045593 normal // chrome: 33076376370 normal android // chrome: 485041940 incognito // chrome: 204974075 incognito android // ToDo: check if FF97+ new storage quota is based on disk or free disk space size let t0 = performance.now() try { if (bBlock) {bB = 1} navigator.storage.estimate().then(estimate => { let quota = estimate.quota, bool = false if (isFF) { if (isVer > 96) { bool = zNA quota = zNA +": variable in FF97+ "+ s3 +"["+ quota +"]"+ sc } else { if (quota > 2147000000 && quota < 2148000000) {bool = true} } } else { bool = zNA quota += " [FF96 or lower]" } fnRecord(num, title, quota, bool, "skip", t0) }) } catch(e) { if (isEngine == "webkit") { fnRecord(num, title, e.name, false, e.message) // not supported in webkit } else { // FF56 or lower let result = e.name let type = zErr let error = e.message if (isFF && isVer < 57 && e.message == "navigator.storage is undefined") { result = zNA +": FF57+ required" type = zNA error = undefined } fnRecord(num, title, result, type, error) } } } function get_window_moz(num) { let strC = "[CSSMozDocumentRule] window" try { let t0C = performance.now() if (bBlock) {bB = 1} let resC = typeof CSSMozDocumentRule let boolC = "function" === typeof CSSMozDocumentRule // FF53+ if (!boolC && isVer == 52) { boolC = "object" === resC } fnRecord(num, strC, resC, boolC, "skip", t0C) } catch(e) { fnRecord(num, strC, e.name, zErr, e.message) } let strM = "[mozRTCPeerConnection] window" try { let t0M = performance.now() if (bBlock) {bB = 1} let resM = typeof mozRTCPeerConnection let boolM = "function" === typeof mozRTCPeerConnection if (!boolM && isFF && "undefined" === resM) { // TB/PM build without WebRTC if (undefined == window.RTCPeerConnection) { boolM = zNA resM = zNA +": --disable-webrtc"+ s3 +" ["+ resM +"]"+ sc } else if (isVer > 112) { // 1531812 boolM = zNA resM = zNA +": FF112 or lower"+ s3 +" ["+ resM +"]"+ sc } } else if (!isFF) { boolM = zNA resM += " [FF112 or lower]" } fnRecord(num+1, strM, resM, boolM, "skip", t0M) } catch(e) { fnRecord(num+1, strM, e.name, zErr, e.message) } let strA = "[mozInnerScreenX] window", boolA = false try { let t0A = performance.now() if (bBlock) {bB = 1} let resA = window.mozInnerScreenX if (!isNaN(resA)) {boolA = true} else {resA = fnClean(resA)} fnRecord(num+2, strA, resA, boolA, "skip", t0A) } catch(e) { fnRecord(num+2, strA, e.name, zErr, e.message) } let strB = "[mozInnerScreenY] window", boolB = false try { let t0B = performance.now() if (bBlock) {bB = 1} let resB = window.mozInnerScreenY if (!isNaN(resB)) {boolB = true} else {resB = fnClean(resB)} fnRecord(num+3, strB, resB, boolB, "skip", t0B) } catch(e) { fnRecord(num+3, strB, e.name, zErr, e.message) } } function get_window_props(num1, num2, title) { // conservative small subset of items from engineprops PoC function rec(type, value, name) { if (value !== "undefined") { if (type == num1) {resE.push(name)} else {resU.push(name)} } } // expected let t0E = performance.now() let resE = [] try { if (bBlock) {bB = 1} // enumerate rec(num1, typeof KeyEvent, "KeyEvent") rec(num1, typeof dump, "dump") rec(num1, typeof fullScreen, "fullScreen") rec(num1, typeof getDefaultComputedStyle, "getDefaultComputedStyle") rec(num1, typeof mozInnerScreenX, "mozInnerScreenX") rec(num1, typeof mozInnerScreenY, "mozInnerScreenY") rec(num1, typeof onabsolutedeviceorientation, "onabsolutedeviceorientation") // removed FF110 1689631 //rec(num1, typeof onbeforeinput, "onbeforeinput") rec(num1, typeof onmozfullscreenchange, "onmozfullscreenchange") rec(num1, typeof onmozfullscreenerror, "onmozfullscreenerror") rec(num1, typeof scrollByLines, "scrollByLines") rec(num1, typeof scrollByPages, "scrollByPages") rec(num1, typeof scrollMaxX, "scrollMaxX") rec(num1, typeof scrollMaxY, "scrollMaxY") rec(num1, typeof setResizable, "setResizable") rec(num1, typeof sizeToContent, "sizeToContent") // removed nightly FF117+ 1832733 / 1600400 rec(num1, typeof u2f, "u2f") rec(num1, typeof updateCommands, "updateCommands") // finish let boolE = resE.length > 0 let hashE = boolE ? mini(resE) : "none" if (boolE) { if (resE.join(", ").length < 48) { hashE = resE.join(", ") } else { sDetail["expected_"+ title] = resE hashE += buildButton(3, "expected_"+ title, resE.length) } } fnRecord(num1, title, hashE, boolE, "skip", t0E) } catch(e) { fnRecord(num1, title, e.name, zErr, e.message) } // unexpected let t0U = performance.now() let resU = [] try { if (bBlock) {bB = 1} // enumerate // webkit only rec(num2, typeof browser, "browser") rec(num2, typeof getMatchedCSSRules, "getMatchedCSSRules") rec(num2, typeof safari, "safari") rec(num2, typeof showModalDialog, "showModalDialog") rec(num2, typeof webkitCancelRequestAnimationFrame, "webkitCancelRequestAnimationFrame") rec(num2, typeof webkitConvertPointFromNodeToPage, "webkitConvertPointFromNodeToPage") rec(num2, typeof webkitConvertPointFromPageToNode, "webkitConvertPointFromPageToNode") rec(num2, typeof webkitIndexedDB, "webkitIndexedDB") // blink only rec(num2, typeof Keyboard, "Keyboard") rec(num2, typeof KeyboardLayoutMap, "KeyboardLayoutMap") rec(num2, typeof PERSISTENT, "PERSISTENT") rec(num2, typeof TEMPORARY, "TEMPORARY") rec(num2, typeof chrome, "chrome") rec(num2, typeof cookieStore, "cookieStore") // FF132 nightly 1918643 rec(num2, typeof getScreenDetails, "getScreenDetails") rec(num2, typeof launchQueue, "launchQueue") rec(num2, typeof navigation, "navigation") // FF146 nightly 1979288 rec(num2, typeof offscreenBuffering, "offscreenBuffering") rec(num2, typeof onappinstalled, "onappinstalled") rec(num2, typeof onbeforeinstallprompt, "onbeforeinstallprompt") rec(num2, typeof onbeforematch, "onbeforematch") // FF139 1761043 dom.hidden_until_found.enabled rec(num2, typeof onbeforexrselect, "onbeforexrselect") rec(num2, typeof oncontextlost, "oncontextlost") rec(num2, typeof oncontextrestored, "oncontextrestored") rec(num2, typeof ondeviceorientationabsolute, "ondeviceorientationabsolute") // FF110+ 1689631 rec(num2, typeof onsearch, "onsearch") rec(num2, typeof openDatabase, "openDatabase") rec(num2, typeof opr, "opr") // OPERA desktop at least rec(num2, typeof originAgentCluster, "originAgentCluster") // FF138+ 1665474 rec(num2, typeof queryLocalFonts, "queryLocalFonts") rec(num2, typeof showDirectoryPicker, "showDirectoryPicker") rec(num2, typeof showOpenFilePicker, "showOpenFilePicker") rec(num2, typeof showSaveFilePicker, "showSaveFilePicker") rec(num2, typeof trustedTypes, "trustedTypes") // FF145 nightly 1955251 rec(num2, typeof webkitRequestFileSystem, "webkitRequestFileSystem") rec(num2, typeof webkitResolveLocalFileSystemURL, "webkitResolveLocalFileSystemURL") rec(num2, typeof webkitStorageInfo, "webkitStorageInfo") // finish if (isVer > 109) { // FF110+ 1689631 resU = resU.filter(x => !["ondeviceorientationabsolute"].includes(x)) if (isVer > 125) { // FF126: 1887729 resU = resU.filter(x => !["oncontextlost"].includes(x)) resU = resU.filter(x => !["oncontextrestored"].includes(x)) } if (isVer > 131) {resU = resU.filter(x => !["cookieStore"].includes(x))} // FF132 nightly 1918643 if (isVer > 137) {resU = resU.filter(x => !["originAgentCluster"].includes(x))} if (isVer > 138) {resU = resU.filter(x => !["onbeforematch"].includes(x))} if (isVer > 144) {resU = resU.filter(x => !["trustedTypes"].includes(x))} // FF145 nightly 1955251 if (isVer > 145) {resU = resU.filter(x => !["navigation"].includes(x))} // FF146 nightly 1979288 dom.navigation.webidl.enabled } let boolU = resU.length == 0 let hashU = !boolU ? mini(resU) : "none" if (!boolU) { if (resU.join(", ").length < 48) { hashU = resU.join(", ") } else { sDetail["unexpected_"+ title] = resU hashU += buildButton(3, "unexpected_"+ title, resU.length) } } fnRecord(num2, title, hashU, boolU, "skip", t0U) } catch(e) { fnRecord(num2, title, e.name, zErr, e.message) } } function rerun() { // reset results = [] itemnumbers = [], data = {}, aPerf = [], count = 0, dom.perf = "" dom.results = "" // do it: delay for user to see visual change setTimeout(function() { run() }, 120) } function run() { // now get on with it fnRecord(0, "header", "expected") fnRecord(300, "header", "not expected") fnRecord(600, "header", "specific values") fnRecord(899, "hr") fnRecord(900, "header", "errors") // OK, let's kick some ass // stagger to get more accurate perf setTimeout(function() { gt0 = performance.now() aPerf.push(["START 1", gt0]) get_installtrigger(50) // 50-52 get_eval_length(630, "eval.toString().length") get_colorgamut(310, "[css] color-gamut") get_nav_screen(170) get_window_props(260, 450, "[properties] window") get_obj_enumeration(750, "object enumeration") get_last_prototype_keys(640, "[last x keys]") // 640+641 get_math(720, "math") get_nav_values(150, 400, 740) // 100 expected, 400-410 not expected, 740.. specific get_dates(620) get_js_client_hints(320, "[js] client hints") get_mimetypes(730, "mimetypes") get_plugins(770, "plugins") get_media_constraints(725, "[constraints] mediaDevices") }, 1) setTimeout(function() { aPerf.push(["START 2", performance.now(), performance.now() - gt0]) get_moz_objects(88, "CanvasRenderingContext2D") get_moz_objects(94, "CSS2|CSSStyle") get_moz_objects(96, "DataTransfer") get_moz_objects(98, "Document") get_moz_objects(100, "Element") get_moz_objects(106, "HTMLElement") get_moz_objects(108, "HTMLCanvasElement") get_moz_objects(110, "HTMLInputElement") get_moz_objects(112, "HTMLMediaElement") get_moz_objects(114, "HTMLVideoElement") get_moz_objects(116, "IDBIndex") get_moz_objects(118, "IDBObjectStore") get_moz_objects(120, "MathMLElement") // added in FF71 get_moz_objects(122, "MouseEvent") get_moz_objects(126, "Navigator") // ^media.peerconnection.enabled + media.navigator.enabled = false negative get_moz_objects(128, "OfflineResourceList") // ^pref: browser.cache.offline.enable [pref exists since Jesus] // nightly/beta/dev default off - since when? FF71+ 1237782 ? get_moz_objects(130, "Screen") get_moz_objects(132, "ShadowRoot") // added in FF63 get_moz_objects(134, "SVGElement") get_moz_objects(136, "XMLHttpRequest") }, 1) setTimeout(function() { aPerf.push(["START 3", performance.now(), performance.now() - gt0]) get_window_moz(250) // 250-253 get_nav_keys(85, 350, "[navigator] keys") // 85 expected, 350 not expected get_intl_supportedlocales(655) // +5's: C, DTF, DN, LF, NF, PR, RTF get_intl_canonical_locale(667, "Intl.getCanonicalLocales") get_locale_compare(710, "locale.compare") get_intl_supportedvaluesof(700, "Intl.supportedValuesOf") // timeZone }, 1) setTimeout(function() { aPerf.push(["START 4", performance.now(), performance.now() - gt0]) get_moz_colors(90, "[colors] moz") get_moz_fonts(102, "[fonts] moz") get_errors(902) get_stacklength(30,850,901) // 30-32 error properties, 850 = stack length, 901 = first error message }, 1) setTimeout(function() { aPerf.push(["START THE END", performance.now(), performance.now() - gt0]) get_iframe_props(40, 330, "[properties] iframe") // 40 expected, 330 not expected, }, 1) setTimeout(function() { get_permission(800, "[permissions] push") get_storage_estimate(870, "storage estimate") }, 1) } setTimeout(function() { Promise.all([ get_globals() ]).then(function(){ Promise.all([ get_is95(), get_isVer(), ]).then(function(){ run() }) }) }, 50) </script> </body> </html> ================================================ FILE: tests/engineprop.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=900"> <title>engine properties</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <style> table {width: 90%; min-width: 600px; max-width: 880px} #tb3 td:first-child { text-align: left; vertical-align: top;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#feature">return to TZP index</a></td></tr> </table> <table id="tb3"> <col width="50%"><col width="50%"> <thead><tr><th colspan="2"> <div class="nav-title">engine properties <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="mono" style="text-align: left; vertical-align: top;"> <span class="btn3 btnfirst" onClick="rerun()">[ run test ]</span> <span class="btn3 btn" onClick="logConsole(`list`)">[ log list ]</span> | <span class="btn3 btn" onClick="scrape()">[ scrape ]</span> | <span class="btn3 btn"> <select name="items" id="items"> <option value="w">caches</option> <option value="w">console</option> <option value="w">crypto</option> <option value="w">CSS</option> <option value="p">CSS2Properties</option> <option value="p">CSSStyleProperties</option> <option value="p">CanvasRenderingContext2D</option> <option value="w">clientInformation</option> <option value="p">Element</option> <option value="p">Headers</option> <option value="w">navigator</option> <option value="w">performance</option> <option value="w">screen</option> </select> <span onClick="get_engineItem()">[ run item ]</span> </span> | <span class="btn3 btn" onClick="copyclip(`results`)">[ copy ]</span> <br><br> <hr> <br> <span class="spaces" style="color: #b3b3b3;" id="results"></span> </td> </tr> </table> <br> <script> 'use strict'; let oTypes = {}, aList = [], aNew = [], // level 1 items found in scrape not in the test aErr = [], // invalid group codes aType = [], // missing a typeof check tstart, padNum = 14, domresult = document.getElementById("results"), domperf = document.getElementById("perf"), s3 = "<span class='s3'>", s9 = "<span class='s9'>", s12 = "<span class='s12'>", s14 = "<span class='s14'>", s17 = "<span class='s17'>", sb = "<span class='bad'>", sg = "<span class='good'>", sc = "</span>", green_tick = "<span style='font-size: 10px;'><b>" + s9 +" \u2713"+ sc + "</b></span>", red_cross = "<span style='font-size: 10px;'><b>" + sb +" \u2715"+ sc + "</b></span>" /** functions from generic **/ // we don't want to include globals or generic which will pollute scraping function json_highlight(json) { if (typeof json != 'string') { json = json_stringify(json); } json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { var cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; // color undefined (aka "typeof undefined") //if (match == "\"typeof undefined\"") {cls = 'null';} } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="'+ cls +'">'+ match +'</span>'; }) } function json_stringify(passedObj, options = {}) { /* https://github.com/lydell/json-stringify-pretty-compact */ const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g; const indent = JSON.stringify( [1], undefined, options.indent === undefined ? 2 : options.indent ).slice(2, -3); const maxLength = indent === "" ? Infinity : options.maxLength === undefined ? 65 // was 80 : options.maxLength; let { replacer } = options; return (function _stringify(obj, currentIndent, reserved) { if (obj && typeof obj.toJSON === "function") { obj = obj.toJSON(); } // display undefined under an alias so we always have the right number of values // this is just a display, it does not alter the fingerprint data //if (obj === undefined) {obj = "typeof undefined"} const string = JSON.stringify(obj, replacer); if (string === undefined) { return string; } const length = maxLength - currentIndent.length - reserved; if (string.length <= length) { const prettified = string.replace( stringOrChar, (match, stringLiteral) => { return stringLiteral || `${match} `; } ); if (prettified.length <= length) { return prettified; } } if (replacer != null) { obj = JSON.parse(string); replacer = undefined; } if (typeof obj === "object" && obj !== null) { const nextIndent = currentIndent + indent; const items = []; let index = 0; let start; let end; if (Array.isArray(obj)) { start = "["; end = "]"; const { length } = obj; for (; index < length; index++) { items.push( _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) || "null" ); } } else { start = "{"; end = "}"; const keys = Object.keys(obj); const { length } = keys; for (; index < length; index++) { const key = keys[index]; const keyPart = `${JSON.stringify(key)}: `; const value = _stringify( obj[key], nextIndent, keyPart.length + (index === length - 1 ? 0 : 1) ); if (value !== undefined) { items.push(keyPart + value); } } } if (items.length > 0) { return [start, indent + items.join(`,\n${nextIndent}`), end].join( `\n${currentIndent}` ); } } return string; })(passedObj, "", 0); } function mini(str) { // https://stackoverflow.com/a/22429679 const json = `${JSON.stringify(str)}` let i, len, hash = 0x811c9dc5 for (i = 0, len = json.length; i < len; i++) { hash = Math.imul(31, hash) + json.charCodeAt(i) | 0 } return ('0000000' + (hash >>> 0).toString(16)).slice(-8) } function copyclip(element) { // fallback: e.g FF62- function copyExec() { if (document.selection) { let range = document.body.createTextRange() range.moveToElementText(document.getElementById(element)) range.select().createTextRange() document.execCommand("copy") } else if (window.getSelection) { let range = document.createRange() range.selectNode(document.getElementById(element)) window.getSelection().addRange(range) document.execCommand("copy") } } // clipboard API if ("clipboard" in navigator) { try { let content = document.getElementById(element).innerHTML // remove spans, change linebreaks let regex = /<br\s*[\/]?>/gi content = content.replace(regex, "\r\n") content = content.replace(/<\/?span[^>]*>/g,"") content = content.replace(/<\/?hr[^>]*>/g,"") // get it navigator.clipboard.writeText(content).then(function() { // clipboard successfully set }, function() { // clipboard write failed copyExec() }) } catch(e) { copyExec() } } else { copyExec() } } /** function for this test **/ function collectProps () { const objCache = new Set(); // skip these objCache.add(window.self); // prevent us decending into the DOM objCache.add(document); objCache.add(document.activeElement); //objCache.add(event?.currentTarget); // FF73 or lower this causes a syntax error if (self.event && event.currentTarget) objCache.add(event.currentTarget) const recursionAllowed = new Set(); recursionAllowed.add('navigator'); function extractObjectProps (obj, breadcrumbs = [], depth = 0) { if (depth >= 5) { return {}; } const props = {}; const proplist = []; for (const prop in obj) { proplist.push(prop); } proplist.sort().forEach((prop) => { try { const desc = Object.getOwnPropertyDescriptor(obj, prop) || {}; desc.type = typeof obj[prop]; if (desc.type === 'function') { desc.value = obj[prop].toString(); } else if (desc.type === 'object' && prop !== "opener") { // don't enuemrate opener object if (!objCache.has(obj[prop]) || recursionAllowed.has(prop)) { objCache.add(obj[prop]); desc.properties = extractObjectProps(obj[prop], [...breadcrumbs, prop], depth + 1); } delete desc.value; } props[prop] = desc; } catch (e) { console.warn(`Error for ${breadcrumbs.join('.')}.${prop}`, e); } }); return props; } return { window: extractObjectProps(window) } } function get_engineProp(item) { try { let objCache = new Set() let order = {} objCache.add(window.self) // prevent us decending into the DOM objCache.add(document); objCache.add(document.activeElement); //objCache.add(event?.currentTarget); // FF73 or lower this causes a syntax error if (self.event && event.currentTarget) objCache.add(event.currentTarget) function get_perf_props(item) { const recursionAllowed = new Set(); recursionAllowed.add('navigator'); function extractObjectProps (obj, breadcrumbs = [], depth = 0) { if (depth >= 5) { return {} } const props = {} const proplist = [] for (const prop in obj) { proplist.push(prop) } let proptemp = [] proplist.forEach((prop) => { proptemp.push(prop) }) if (proptemp.length) { if ("object" === typeof obj && obj !== null) { let name = (obj+"").slice(8, -1) order[name] = {} order[name]["count"] = proptemp.length order[name]["hash"] = mini(proptemp) order[name]["data"] = proptemp } } proplist.sort().forEach((prop) => { try { const desc = Object.getOwnPropertyDescriptor(obj, prop) || {} if (typeof obj[prop] === 'function') { desc.length = obj[prop].toString().length } desc.type = typeof obj[prop] if (desc.type === 'function') { desc.value = obj[prop].toString() } else if (desc.type === 'object' && prop !== "opener") { // don't enuemrate opener object //if (!objCache.has(obj[prop])) { // || recursionAllowed.has(prop)) { if (!objCache.has(obj[prop]) || recursionAllowed.has(prop)) { objCache.add(obj[prop]) desc.properties = extractObjectProps(obj[prop], [...breadcrumbs, prop], depth + 1) } delete desc.value } props[prop] = desc } catch (e) { console.warn(`Error for ${breadcrumbs.join('.')}.${prop}`, e) } }) return props } return extractObjectProps(item) } let data = get_perf_props(item) let datahash = mini(data) let oData = { "data": { hash: datahash, metrics: data, } } if (Object.keys(order).length) { // reorder order let neworder = {} for (const k of Object.keys(order).sort()) {neworder[k] = order[k]} let orderhash = mini(neworder) oData["order"] = {"hash": orderhash, "metrics": neworder} } return oData } catch(e) { return e+"" } } function get_engineItem() { domresult.innerHTML = "" domperf.innerHTML = "" setTimeout(function() { tstart = performance.now() let selection = document.getElementById("items") let type = selection.value let name = selection.options[selection.selectedIndex].text let displayname = s3 + name + sc + ": " let item try { if ("w" === type) { item = window[name] } else if ("p" === type) { item = window[name].prototype } if (item !== undefined) { let oData = get_engineProp(item) if ("string" == typeof oData) { domresult.innerHTML = displayname + oData } else { let hash = mini(oData) domperf.innerHTML = Math.round((performance.now() - tstart)) +" ms" domresult.innerHTML = displayname + hash + "<br><br>" + json_highlight(oData) } } else { domresult.innerHTML = displayname + "oophs... not coded yet" } } catch(e) { domresult.innerHTML = displayname + e+"" } }, 170) } function logConsole(name) { let array = [], str = "" if (name == "list") { array = aList; str = "TEST LIST" } console.log(str +" [" + array.length +"]\n" + array.join("\n")) } function output() { domperf.innerHTML = Math.round((performance.now() - tstart)) +" ms | "+ aList.length +" items" aList.sort() let display = [] for (const k of Object.keys(oTypes).sort()) { let group = oTypes[k] let gHash = [] let gDisplay = [] let gCount = 0 let gCountUndefined = 0 // get group details for (const n of Object.keys(group).sort()) { let data = group[n] data.sort() gCount += data.length if (data.length) { let tHash = mini(data) gHash.push(n +": "+ tHash) gDisplay.push(s3 + n.padStart(padNum) + sc +": "+ tHash + s3 +" ["+ data.length +"]"+ sc +"<br>") gDisplay.push(data.join(", ") + "<br>") if (n == "undefined") { gCountUndefined += data.length } } } if (gDisplay.length) { // get group hash let hashG = gHash.length ? mini(gHash) +" ["+ gCount +"]" : "" // add group header let strG = "", strExtra = "", strColor = s12 //only if (k == 10) {strG = "BLINK ONLY" } else if (k == 11) { strG = "EDGEHTML ONLY" } else if (k == 12) { strG = "GECKO ONLY" } else if (k == 14) { strG = "WEBKIT ONLY" // in two but not the other } else if (k == 50) { strG = "BLINK + GECKO"; strColor = s14; strExtra = " [not webkit... yet]" } else if (k == 52) { strG = "BLINK + WEBKIT"; strColor = s14; strExtra = " [not gecko... yet]" } else if (k == 54) { strG = "GECKO + WEBKIT"; strColor = s14; strExtra = " [not blink... yet]" // the rest } else if (k == 98) { strG = "UNDETERMINED"; strColor = s17; strExtra = " to be assessed" } else if (k == 99) { strG = "IGNORE"; strColor = s17; strExtra = " confirmed in all three engines" } strG = strG.padStart(padNum) let header = strG + (gHash.length ? ": "+ hashG : "") // add a green "all undefined" - we should get at least three per engine let strU = "" if (gHash.length && k < 90) { if (gCount == gCountUndefined) { strU = " "+ green_tick + sg +" all "+ gCountUndefined +" undefined"+ sc } } display.push(strColor + header + strExtra + sc + strU +"<br>") // add group detail if (gHash.length) { display.push(gDisplay.join("<br>")) } // hr if (k < 99) { display.push("<br><hr>") } } } // DEV ALERTS let strAlert = "" // invalid group codes if (aErr.length) { strAlert += sb + "INVALID GROUP CODES: " + sc + aErr.join(", ") +"<br><br>" } // missing typeof parameter if (aType.length) { strAlert += sb + "MSSING TYPEOF CHECK: " + sc + aType.join(", ") +"<br><br>" } // dupes let aDupe = aList aList = aList.filter(function(item, position) {return aList.indexOf(item) === position}) let hDupe = mini(aDupe.join()) let hList = mini(aList.join()) let strDupe = "" if (hList !== hDupe) { aDupe = aDupe.filter((item, index) => aDupe.indexOf(item) !== index) strAlert += sb + "DUPLICATES: " + sc + aDupe.join(", ") + "<br><br>" } // alerts if (strAlert !== "") {strAlert += "<hr><br>"} // function hash // gecko: 39: 334c4512 : function atob() {\n [native code]\n} // blink: 33: 1dd1c7f6 : function atob() { [native code] } // edgehtml: 33: 1dd1c7f6 : function atob() { [native code] } let strF = "" try { let testF = atob.toString() testF = testF.replace(/\n/g,'\\n'); let hashF = "length "+ testF.length + ": " + mini(testF) strF = s14 + ("FUNCTION").padStart(padNum) + sc +": "+ hashF + " : " + testF + "<br><br><hr><br>" } catch(e) { console.error(e.name, e.message) } // output let strM = s14 + ("MINI TEST").padStart(padNum) + sc + ": "+ isEnginePretty + "<br><br>" domresult.innerHTML = strAlert + strM + strF + display.join("<br>") } function rec(type, name, group = 98, expected) { aList.push(name) try { if (expected !== undefined) { if (type !== "undefined" && type !== expected) { name = sb + name +" ["+ expected +"]"+ sc } } else { // missing typeof parameter aType.push(name) } oTypes[group][type].push(name) } catch(e) { aErr.push(name) oTypes[98][type].push(name) } } function reset() { aList = [] aErr = [] aType = [] domresult.innerHTML = "" oTypes = {} let groups = [10,11,12,14,50,52,54,98,99] let types = ["bigint","boolean","function","number","object","string","symbol","undefined",] groups.forEach(function(group) { oTypes[group] = {} types.forEach(function(type) { oTypes[group][type] = [] }) }) } function rerun() { reset() // delay so user sees changes setTimeout(function() { run() }, 170) } function run() { try { tstart = performance.now() let code /* LEVEL ONE */ // undetermined: status confirmed in EOL comments code = 98 //rec(typeof keys, "keys", code) // NFI where I got this from // blink only code = 10 rec(typeof Keyboard, "Keyboard", code, "function") rec(typeof KeyboardLayoutMap, "KeyboardLayoutMap", code, "function") rec(typeof PERSISTENT, "PERSISTENT", code, "number") rec(typeof TEMPORARY, "TEMPORARY", code, "number") rec(typeof chrome, "chrome", code, "object") // edgeHTML rec(typeof onappinstalled, "onappinstalled", code, "object") rec(typeof onbeforeinstallprompt, "onbeforeinstallprompt", code, "object") rec(typeof openDatabase, "openDatabase", code, "function") rec(typeof webkitRequestFileSystem, "webkitRequestFileSystem", code, "function") rec(typeof webkitResolveLocalFileSystemURL, "webkitResolveLocalFileSystemURL", code, "function") // android rec(typeof getDigitalGoodsService, "getDigitalGoodsService", code, "function") // not in Brave (or android) rec(typeof showDirectoryPicker, "showDirectoryPicker", code, "function") rec(typeof showOpenFilePicker, "showOpenFilePicker", code, "function") rec(typeof showSaveFilePicker, "showSaveFilePicker", code, "function") // newish: not in v90 rec(typeof getScreenDetails, "getScreenDetails", code, "function") rec(typeof launchQueue, "launchQueue", code, "object") // + not in Brave (or android) rec(typeof onbeforexrselect, "onbeforexrselect", code, "object") rec(typeof queryLocalFonts, "queryLocalFonts", code, "function") rec(typeof sharedStorage, "sharedStorage", code, "object") // newish: since last check; testing chrome 119 rec(typeof fence, "fence", code, "object") rec(typeof credentialless, "credentialless", code, "boolean") // other rec(typeof opr, "opr", code, "object") // OPERA (desktop at least) rec(typeof webkitStorageInfo, "webkitStorageInfo", code, "object") // to-be-deprecated see console // stuff showing up in scrape > new rec(typeof fetchLater, "fetchLater", code, "function") rec(typeof onpagereveal, "onpagereveal", code, "object") rec(typeof onpageswap, "onpageswap", code, "object") rec(typeof onscrollsnapchange, "onscrollsnapchange", code, "object") rec(typeof onscrollsnapchanging, "onscrollsnapchanging", code, "object") rec(typeof viewport, "viewport", code, "object") rec(typeof when, "when", code, "function") // edgeHTML only code = 11 /* notes: has blink only: chrome, offscreenBuffering gecko only: ondevicelight, onvrdisplayactivate, onvrdisplayconnect, onvrdisplaydeactivate, onvrdisplaydisconnect, onvrdisplaypresentchange webkit only: browser, getMatchedCSSRules, webkitConvertPointFromNodeToPage, webkitConvertPointFromPageToNode also pairs are a mess */ rec(typeof clearImmediate, "clearImmediate", code, "function") rec(typeof msWriteProfilerMark, "msWriteProfilerMark", code, "function") rec(typeof oncompassneedscalibration, "oncompassneedscalibration", code, "object") rec(typeof onmsgesturechange, "onmsgesturechange", code, "object") rec(typeof onmsgesturedoubletap, "onmsgesturedoubletap", code, "object") rec(typeof onmsgestureend, "onmsgestureend", code, "object") rec(typeof onmsgesturehold, "onmsgesturehold", code, "object") rec(typeof onmsgesturestart, "onmsgesturestart", code, "object") rec(typeof onmsgesturetap, "onmsgesturetap", code, "object") rec(typeof onmsinertiastart, "onmsinertiastart", code, "object") rec(typeof onreadystatechange, "onreadystatechange", code, "object") rec(typeof onvrdisplayblur, "onvrdisplayblur", code, "object") rec(typeof onvrdisplayfocus, "onvrdisplayfocus", code, "object") rec(typeof onvrdisplaypointerrestricted, "onvrdisplaypointerrestricted", code, "object") rec(typeof onvrdisplaypointerunrestricted, "onvrdisplaypointerunrestricted", code, "object") rec(typeof setImmediate, "setImmediate", code, "function") // gecko only code = 12 // stable FF52+ rec(typeof KeyEvent, "KeyEvent", code, "function") rec(typeof dump, "dump", code, "function") rec(typeof fullScreen, "fullScreen", code, "boolean") rec(typeof getDefaultComputedStyle, "getDefaultComputedStyle", code, "function") rec(typeof mozInnerScreenX, "mozInnerScreenX", code, "number") rec(typeof mozInnerScreenY, "mozInnerScreenY", code, "number") rec(typeof onmozfullscreenchange, "onmozfullscreenchange", code, "object") rec(typeof onmozfullscreenerror, "onmozfullscreenerror", code, "object") rec(typeof scrollByLines, "scrollByLines", code, "function") rec(typeof scrollByPages, "scrollByPages", code, "function") rec(typeof scrollMaxX, "scrollMaxX", code, "number") rec(typeof scrollMaxY, "scrollMaxY", code, "number") rec(typeof setResizable, "setResizable", code, "function") rec(typeof updateCommands, "updateCommands", code, "function") // deprecated rec(typeof mozPaintCount, "mozPaintCount", code, "number") // removed FF72 rec(typeof onshow, "onshow", code, "object") // removed FF85 (+ not goanna or Waterfox Classic) rec(typeof ondevicelight, "ondevicelight", code, "object") // removed FF89 rec(typeof ondeviceproximity, "ondeviceproximity", code, "object") // removed FF89 rec(typeof onuserproximity, "onuserproximity", code, "object") // removed FF89 rec(typeof content, "content", code, "object") // removed FF101 rec(typeof sidebar, "sidebar", code, "object") // removed FF102 rec(typeof onloadend, "onloadend", code, "object") // removed FF107 1574487 rec(typeof InstallTrigger, "InstallTrigger", code, "object") // to-be-deprecated (+prefs) rec(typeof onabsolutedeviceorientation, "onabsolutedeviceorientation", code, "object") // removed FF110 1689631 rec(typeof sizeToContent, "sizeToContent", code, "function") // removed nightly FF117+ 1832733 / 1600400 // prefs rec(typeof onvrdisplayactivate, "onvrdisplayactivate", code, "object") // dom.vr.enabled (+ not in FF52) rec(typeof onvrdisplayconnect, "onvrdisplayconnect", code, "object") rec(typeof onvrdisplaydeactivate, "onvrdisplaydeactivate", code, "object") // (+ not in FF52) rec(typeof onvrdisplaydisconnect, "onvrdisplaydisconnect", code, "object") rec(typeof onvrdisplaypresentchange, "onvrdisplaypresentchange", code, "object") rec(typeof u2f, "u2f", code, "object") // security.webauth.u2f | pref removed in FF114 // webkit only code = 14 rec(typeof browser, "browser", code, "object") rec(typeof getMatchedCSSRules, "getMatchedCSSRules", code, "function") rec(typeof safari, "safari", code, "object") rec(typeof showModalDialog, "showModalDialog", code, "function") rec(typeof webkitCancelRequestAnimationFrame, "webkitCancelRequestAnimationFrame", code, "function") rec(typeof webkitConvertPointFromNodeToPage, "webkitConvertPointFromNodeToPage", code, "function") rec(typeof webkitConvertPointFromPageToNode, "webkitConvertPointFromPageToNode", code, "function") rec(typeof webkitIndexedDB, "webkitIndexedDB", code, "object") // blink + gecko (not seen in webkit) code = 50 rec(typeof cancelIdleCallback, "cancelIdleCallback", code, "function") rec(typeof external, "external", code, "object") rec(typeof onbeforematch, "onbeforematch", code, "object") // FF139+ 1761043 rec(typeof oncommand, "oncommand", code, "object") // FF144+ rec(typeof ondeviceorientationabsolute, "ondeviceorientationabsolute", code, "object") // FF110+ 1689631 rec(typeof ondragexit, "ondragexit", code, "object") // touch rec(typeof onorientationchange, "onorientationchange", code, "object") // mobile/tablet rec(typeof navigation, "navigation", code, "object") // // FF146 nightly 1979288 rec(typeof onscrollend, "onscrollend", code, "object") rec(typeof ontouchcancel, "ontouchcancel", code, "object") rec(typeof ontouchend, "ontouchend", code, "object") rec(typeof ontouchmove, "ontouchmove", code, "object") rec(typeof ontouchstart, "ontouchstart", code, "object") rec(typeof orientation, "orientation", code, "number") rec(typeof requestIdleCallback, "requestIdleCallback", code, "function") rec(typeof scheduler, "scheduler", code, "object") rec(typeof trustedTypes, "trustedTypes", code, "object") // FF145 nightly 1955251 rec(typeof oncontextlost, "oncontextlost", code, "object") rec(typeof oncontextrestored, "oncontextrestored", code, "object") rec(typeof originAgentCluster, "originAgentCluster", code, "boolean") // FF138+ 1665474 rec(typeof onpointerrawupdate, "onpointerrawupdate", code, "object") // FF140+ 1550462 rec(typeof documentPictureInPicture, "documentPictureInPicture", code, "object") // FF148n 1858562 // blink + webkit (not seen in gecko) code = 52 rec(typeof defaultstatus, "defaultstatus", code, "string") rec(typeof defaultStatus, "defaultStatus", code, "string") rec(typeof offscreenBuffering, "offscreenBuffering", code, "boolean") // edgeHTML rec(typeof onmousewheel, "onmousewheel", code, "object") rec(typeof onsearch, "onsearch", code, "object") rec(typeof styleMedia, "styleMedia", code, "object") rec(typeof webkitCancelAnimationFrame, "webkitCancelAnimationFrame", code, "function") rec(typeof webkitRequestAnimationFrame, "webkitRequestAnimationFrame", code, "function") // gecko + webkit (not seen in blink) code = 54 rec(typeof onanimationcancel, "onanimationcancel", code, "object") rec(typeof oncopy, "oncopy", code, "object") // FF110+ rec(typeof oncut, "oncut", code, "object") // FF110+ rec(typeof ongamepadconnected, "ongamepadconnected", code, "object") // FF89+ rec(typeof ongamepaddisconnected, "ongamepaddisconnected", code, "object") // FF89+ rec(typeof onpaste, "onpaste", code, "object") // FF110+ // I: IGNORE: confirmed common in blink, gecko, webkit code = 99 rec(typeof addEventListener, "addEventListener", code, "function") rec(typeof alert, "alert", code, "function") rec(typeof applicationCache, "applicationCache", code, "object") // ^ gecko confirmed: pref-to-be-deprecated (appCache itself is deprecated: the API does nothing) // ^ ignore: appCache would have been in other older engine(s) releases rec(typeof atob, "atob", code, "function") rec(typeof blur, "blur", code, "function") rec(typeof btoa, "btoa", code, "function") rec(typeof caches, "caches", code, "object") rec(typeof cancelAnimationFrame, "cancelAnimationFrame", code, "function") rec(typeof captureEvents, "captureEvents", code, "function") rec(typeof clearInterval, "clearInterval", code, "function") rec(typeof clearTimeout, "clearTimeout", code, "function") rec(typeof clientInformation, "clientInformation", code, "object") rec(typeof close, "close", code, "function") rec(typeof closed, "closed", code, "boolean") rec(typeof confirm, "confirm", code, "function") rec(typeof cookieStore, "cookieStore", code, "object") // FF132 nightly 1918643 rec(typeof createImageBitmap, "createImageBitmap", code, "function") rec(typeof crossOriginIsolated, "crossOriginIsolated", code, "boolean") rec(typeof crypto, "crypto", code, "object") rec(typeof customElements, "customElements", code, "object") rec(typeof devicePixelRatio, "devicePixelRatio", code, "number") rec(typeof dispatchEvent, "dispatchEvent", code, "function") rec(typeof document, "document", code, "object") rec(typeof fetch, "fetch", code, "function") rec(typeof find, "find", code, "function") rec(typeof focus, "focus", code, "function") rec(typeof frameElement, "frameElement", code, "object") rec(typeof frames, "frames", code, "object") rec(typeof getComputedStyle, "getComputedStyle", code, "function") rec(typeof getSelection, "getSelection", code, "function") rec(typeof history, "history", code, "object") rec(typeof indexedDB, "indexedDB", code, "object") rec(typeof innerHeight, "innerHeight", code, "number") rec(typeof innerWidth, "innerWidth", code, "number") rec(typeof isSecureContext, "isSecureContext", code, "boolean") rec(typeof KeyboardEvent, "KeyboardEvent", code, "function") rec(typeof KeyframeEffect, "KeyframeEffect", code, "function") rec(typeof length, "length", code, "number") rec(typeof localStorage, "localStorage", code, "object") rec(typeof location, "location", code, "object") rec(typeof locationbar, "locationbar", code, "object") rec(typeof matchMedia, "matchMedia", code, "function") rec(typeof menubar, "menubar", code, "object") rec(typeof moveBy, "moveBy", code, "function") rec(typeof moveTo, "moveTo", code, "function") rec(typeof name, "name", code, "string") rec(typeof navigator, "navigator", code, "object") rec(typeof onabort, "onabort", code, "object") rec(typeof onafterprint, "onafterprint", code, "object") rec(typeof onanimationend, "onanimationend", code, "object") rec(typeof onanimationiteration, "onanimationiteration", code, "object") rec(typeof onanimationstart, "onanimationstart", code, "object") rec(typeof onauxclick, "onauxclick", code, "object") rec(typeof onbeforeinput, "onbeforeinput", code, "object") rec(typeof onbeforeprint, "onbeforeprint", code, "object") rec(typeof onbeforetoggle, "onbeforetoggle", code, "object") // FF122+ 1867326 (and 1823359) rec(typeof onbeforeunload, "onbeforeunload", code, "object") rec(typeof onblur, "onblur", code, "object") rec(typeof oncancel, "oncancel", code, "object") rec(typeof oncanplay, "oncanplay", code, "object") rec(typeof oncanplaythrough, "oncanplaythrough", code, "object") rec(typeof onchange, "onchange", code, "object") rec(typeof onclick, "onclick", code, "object") rec(typeof onclose, "onclose", code, "object") rec(typeof oncontentvisibilityautostatechange, "oncontentvisibilityautostatechange", code, "object") rec(typeof oncontextmenu, "oncontextmenu", code, "object") rec(typeof oncuechange, "oncuechange", code, "object") rec(typeof ondblclick, "ondblclick", code, "object") rec(typeof ondevicemotion, "ondevicemotion", code, "object") rec(typeof ondeviceorientation, "ondeviceorientation", code, "object") rec(typeof ondrag, "ondrag", code, "object") rec(typeof ondragend, "ondragend", code, "object") rec(typeof ondragenter, "ondragenter", code, "object") rec(typeof ondragleave, "ondragleave", code, "object") rec(typeof ondragover, "ondragover", code, "object") rec(typeof ondragstart, "ondragstart", code, "object") rec(typeof ondrop, "ondrop", code, "object") rec(typeof ondurationchange, "ondurationchange", code, "object") rec(typeof onemptied, "onemptied", code, "object") rec(typeof onended, "onended", code, "object") rec(typeof onerror, "onerror", code, "object") rec(typeof onfocus, "onfocus", code, "object") rec(typeof onformdata, "onformdata", code, "object") rec(typeof ongotpointercapture, "ongotpointercapture", code, "object") rec(typeof onhashchange, "onhashchange", code, "object") rec(typeof oninput, "oninput", code, "object") rec(typeof oninvalid, "oninvalid", code, "object") rec(typeof onkeydown, "onkeydown", code, "object") rec(typeof onkeypress, "onkeypress", code, "object") rec(typeof onkeyup, "onkeyup", code, "object") rec(typeof onlanguagechange, "onlanguagechange", code, "object") rec(typeof onload, "onload", code, "object") rec(typeof onloadeddata, "onloadeddata", code, "object") rec(typeof onloadedmetadata, "onloadedmetadata", code, "object") rec(typeof onloadstart, "onloadstart", code, "object") rec(typeof onlostpointercapture, "onlostpointercapture", code, "object") rec(typeof onmessage, "onmessage", code, "object") rec(typeof onmessageerror, "onmessageerror", code, "object") rec(typeof onmousedown, "onmousedown", code, "object") rec(typeof onmouseenter, "onmouseenter", code, "object") rec(typeof onmouseleave, "onmouseleave", code, "object") rec(typeof onmousemove, "onmousemove", code, "object") rec(typeof onmouseout, "onmouseout", code, "object") rec(typeof onmouseover, "onmouseover", code, "object") rec(typeof onmouseup, "onmouseup", code, "object") rec(typeof onoffline, "onoffline", code, "object") rec(typeof ononline, "ononline", code, "object") rec(typeof onpagehide, "onpagehide", code, "object") rec(typeof onpageshow, "onpageshow", code, "object") rec(typeof onpause, "onpause", code, "object") rec(typeof onplay, "onplay", code, "object") rec(typeof onplaying, "onplaying", code, "object") rec(typeof onpointercancel, "onpointercancel", code, "object") rec(typeof onpointerdown, "onpointerdown", code, "object") rec(typeof onpointerenter, "onpointerenter", code, "object") rec(typeof onpointerleave, "onpointerleave", code, "object") rec(typeof onpointermove, "onpointermove", code, "object") rec(typeof onpointerout, "onpointerout", code, "object") rec(typeof onpointerover, "onpointerover", code, "object") rec(typeof onpointerup, "onpointerup", code, "object") rec(typeof onpopstate, "onpopstate", code, "object") rec(typeof onprogress, "onprogress", code, "object") rec(typeof onratechange, "onratechange", code, "object") rec(typeof onrejectionhandled, "onrejectionhandled", code, "object") rec(typeof onreset, "onreset", code, "object") rec(typeof onresize, "onresize", code, "object") rec(typeof onscroll, "onscroll", code, "object") rec(typeof onsecuritypolicyviolation, "onsecuritypolicyviolation", code, "object") rec(typeof onseeked, "onseeked", code, "object") rec(typeof onseeking, "onseeking", code, "object") rec(typeof onselect, "onselect", code, "object") rec(typeof onselectionchange, "onselectionchange", code, "object") rec(typeof onselectstart, "onselectstart", code, "object") rec(typeof onslotchange, "onslotchange", code, "object") rec(typeof onstalled, "onstalled", code, "object") rec(typeof onstorage, "onstorage", code, "object") rec(typeof onsubmit, "onsubmit", code, "object") rec(typeof onsuspend, "onsuspend", code, "object") rec(typeof ontimeupdate, "ontimeupdate", code, "object") rec(typeof ontoggle, "ontoggle", code, "object") rec(typeof ontransitioncancel, "ontransitioncancel", code, "object") rec(typeof ontransitionend, "ontransitionend", code, "object") rec(typeof ontransitionrun, "ontransitionrun", code, "object") rec(typeof ontransitionstart, "ontransitionstart", code, "object") rec(typeof onunhandledrejection, "onunhandledrejection", code, "object") rec(typeof onunload, "onunload", code, "object") rec(typeof onvolumechange, "onvolumechange", code, "object") rec(typeof onwaiting, "onwaiting", code, "object") rec(typeof onwebkitanimationend, "onwebkitanimationend", code, "object") rec(typeof onwebkitanimationiteration, "onwebkitanimationiteration", code, "object") rec(typeof onwebkitanimationstart, "onwebkitanimationstart", code, "object") rec(typeof onwebkittransitionend, "onwebkittransitionend", code, "object") rec(typeof onwheel, "onwheel", code, "object") rec(typeof open, "open", code, "function") rec(typeof opener, "opener", code, "object") rec(typeof origin, "origin", code, "string") rec(typeof outerHeight, "outerHeight", code, "number") rec(typeof outerWidth, "outerWidth", code, "number") rec(typeof pageXOffset, "pageXOffset", code, "number") rec(typeof pageYOffset, "pageYOffset", code, "number") rec(typeof parent, "parent", code, "object") rec(typeof performance, "performance", code, "object") rec(typeof personalbar, "personalbar", code, "object") rec(typeof postMessage, "postMessage", code, "function") rec(typeof print, "print", code, "function") rec(typeof prompt, "prompt", code, "function") rec(typeof queueMicrotask, "queueMicrotask", code, "function") rec(typeof releaseEvents, "releaseEvents", code, "function") rec(typeof removeEventListener, "removeEventListener", code, "function") rec(typeof reportError, "reportError", code, "function") rec(typeof requestAnimationFrame, "requestAnimationFrame", code, "function") rec(typeof resizeBy, "resizeBy", code, "function") rec(typeof resizeTo, "resizeTo", code, "function") rec(typeof screen, "screen", code, "object") rec(typeof screenLeft, "screenLeft", code, "number") rec(typeof screenTop, "screenTop", code, "number") rec(typeof screenX, "screenX", code, "number") rec(typeof screenY, "screenY", code, "number") rec(typeof scroll, "scroll", code, "function") rec(typeof scrollBy, "scrollBy", code, "function") rec(typeof scrollTo, "scrollTo", code, "function") rec(typeof scrollX, "scrollX", code, "number") rec(typeof scrollY, "scrollY", code, "number") rec(typeof scrollbars, "scrollbars", code, "object") rec(typeof self, "self", code, "object") rec(typeof sessionStorage, "sessionStorage", code, "object") rec(typeof setInterval, "setInterval", code, "function") rec(typeof setTimeout, "setTimeout", code, "function") rec(typeof speechSynthesis, "speechSynthesis", code, "object") rec(typeof status, "status", code, "string") rec(typeof statusbar, "statusbar", code, "object") rec(typeof stop, "stop", code, "function") rec(typeof structuredClone, "structuredClone", code, "function") rec(typeof toolbar, "toolbar", code, "object") rec(typeof top, "top", code, "object") rec(typeof visualViewport, "visualViewport", code, "object") rec(typeof window, "window", code, "object") // LEVEL 2 // I don't think we need bother with level 2 for a mini isEngine /* if ("undefined" !== typeof caches) { rec(typeof caches.delete, "caches.delete") rec(typeof caches.has, "caches.has") rec(typeof caches.keys, "caches.keys") rec(typeof caches.match, "caches.match") rec(typeof caches.open, "caches.open") } */ output() } catch(e) { console.error(e.name, e.message) } } function scrape() { domresult.innerHTML = "<br>... running" aNew = [] // delay so user sees changes setTimeout(function() { tstart = performance.now() let result = collectProps().window // cleanup delete result.event // caused by calling event in the scrape function delete result.dom // just in case delete result.collectProps delete result.compare delete result.copyclip delete result.get_engineItem delete result.get_engineProp delete result.get_miniEngine delete result.json_highlight delete result.json_stringify delete result.logConsole delete result.mini delete result.output delete result.rec delete result.rerun delete result.reset delete result.run delete result.scrape //console.log(result) // sanitize localStorage let aDelLS = [] let sessionGood = ["clear","getItem","key","length","removeItem","setItem"] try { for (const ls of Object.keys(result["localStorage"]["properties"])) { if (!sessionGood.includes(ls)) { delete result["localStorage"]["properties"][ls] aDelSL.push(ls) } } } catch(e) {} // sanitize sessionStorage let aDelSS = [] try { for (const ss of Object.keys(result["sessionStorage"]["properties"])) { if (!sessionGood.includes(ss)) { delete result["sessionStorage"]["properties"][ss] aDelSS.push(ss) } } } catch(e) {} // silently record sanitized let strDel = "" if (aDelSS.length || aDelLS.length) { strDel = sb +"REMOVED: "+ sc +"random state variables<br><br>" + (aDelLS.length ? "localStorage : " + aDelLS.join(", ") +"<br>" : "") + (aDelSS.length ? "sessionStorage: " + aDelSS.join(", ") +"<br>" : "") +"<br>" } // hash it for easier compare let hash = mini(result) domperf.innerHTML = Math.round((performance.now() - tstart)) +" ms" // check if any first level items are not in aList let strNew = "" if (aList.length) { aNew = Object.keys(result) aNew = aNew.filter(x => !aList.includes(x)) if (aNew.length) { strNew = sb +"NEW:"+ sc + "<br><br>" + aNew.join(", ") + sc +"<br><br>" } } domresult.innerHTML = strNew + strDel + s3 + "HASH: "+ sc + hash +"<br><br>" + json_highlight(result) }, 100) } function compare() { // examples: just paste in scrape results let sBlink = {"all3": [], "blink": [], "blink-gecko": [], "blink-webkit": []} let sGecko = {"all3": [], "gecko": [], "blink-gecko": [], "gecko-webkit": []} let sWebkit = {"all3": [], "webkit": [], "blink-webkit": [], "gecko-webkit": []} let aBlink = Object.keys(sBlink) let aGecko = Object.keys(sGecko) let aWebkit = Object.keys(sWebkit) let commonBG = aBlink.filter(x => aGecko.includes(x)) let commonBW = aBlink.filter(x => aWebkit.includes(x)) let commonGW = aGecko.filter(x => aWebkit.includes(x)) let common = commonBG.concat(commonBW) common = common.concat(commonGW) common = common.filter(function(item, position) {return common.indexOf(item) === position}) common.sort() console.log("NOT UNIQUE: "+ common.length +"\n" + common.join("\n")) let common3 = commonBG common3 = commonBG.filter(x => aWebkit.includes(x)) common3 = common3.filter(function(item, position) {return common3.indexOf(item) === position}) common3.sort() console.log("COMMON IN ALL THREE: "+ common3.length +"\n" + common3.join("\n")) let BGonly = commonBG.filter(x => !common3.includes(x)) BGonly.sort() console.log("COMMON IN BLINK + GECKO ONLY: "+ BGonly.length +"\n" + BGonly.join("\n")) let BWonly = commonBW.filter(x => !common3.includes(x)) BWonly.sort() console.log("COMMON IN BLINK + WEBKIT ONLY: "+ BWonly.length +"\n" + BWonly.join("\n")) let GWonly = commonGW.filter(x => !common3.includes(x)) GWonly.sort() console.log("COMMON IN GECKO + WEBKIT ONLY: "+ GWonly.length +"\n" + GWonly.join("\n")) } //compare() function get_miniEngine() { let tstart = performance.now() let oEngines = { "blink": [ "number" === typeof TEMPORARY, "number" === typeof PERSISTENT, "object" === typeof onappinstalled, "object" === typeof onbeforeinstallprompt, //"object" === typeof onpointerrawupdate, //"object" === typeof onsearch, //"boolean" === typeof originAgentCluster, //"object" === typeof trustedTypes, "function" === typeof webkitResolveLocalFileSystemURL, ], "webkit": [ "object" === typeof browser, //"function" === typeof getMatchedCSSRules, "object" === typeof safari, //"function" === typeof showModalDialog, "function" === typeof webkitConvertPointFromNodeToPage, "function" === typeof webkitCancelRequestAnimationFrame, "object" === typeof webkitIndexedDB, ], "gecko": [ "function" === typeof dump, "boolean" === typeof fullScreen, "number" === typeof mozInnerScreenX, "function" === typeof scrollByLines, "number" === typeof scrollMaxY, "function" === typeof setResizable, //"function" === typeof sizeToContent, // removed nightly FF117+ 1832733 / 1600400 "function" === typeof updateCommands, ], "edgeHTML": [ "function" === typeof clearImmediate, "function" === typeof msWriteProfilerMark, "object" === typeof oncompassneedscalibration, "object" === typeof onmsgesturechange, "object" === typeof onmsinertiastart, "object" === typeof onreadystatechange, //"object" === typeof onvrdisplayfocus, "function" === typeof setImmediate, ] } // array engine matches, so subsequent results doesn't override prev let aEngine = [] for (const engine of Object.keys(oEngines).sort()) { let sumE = oEngines[engine].reduce((prev, current) => prev + current, 0) if (sumE > (oEngines[engine].length/2)) {aEngine.push(engine)} } if (aEngine.length == 1) {isEngine = aEngine[0]} // valid one result // perf let tend = performance.now() // re-tidy vars if (isEngine == "gecko") { // check for PM28+ : fails 53 if ("function" !== typeof CSSMozDocumentRule) { isEngine = "goanna" } } // build a pretty display let displayAll = [] for (const engine of Object.keys(oEngines).sort()) { let displayE = [] oEngines[engine].forEach(function(check) { displayE.push(check ? green_tick : red_cross) }) displayAll.push(displayE.join("")) } isEnginePretty = Math.round(tend-tstart) +" ms |"+ displayAll.join(" |") + " | " + (isEngine == "" ? "UNKNOWN" : isEngine.toUpperCase()) } let isEnginePretty = "" let isEngine = "" Promise.all([ get_miniEngine() ]).then(function(){ reset() run() }) </script> </body> </html> ================================================ FILE: tests/fontasync.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>font async</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 580px;} #ugSpan {font-size: 22000px;} .changed { background: rgba(142, 142, 145, 0.3); } .zero { background: rgba(220, 121, 189, 0.3); } .changedzero { background: linear-gradient(rgba(142, 142, 145, 0.4), rgba(142, 142, 145, 0.2), rgba(220, 121, 189, 0.4)); } span.box { display: inline-block; position: relative; margin-top: 2px; width: 47px; height: 60px; border: 1px solid grey; font-size: 24px; text-align: center; vertical-align: top; } span.info { display: block; position: relative; padding-top: 2px; width: 48px; height: 15px; font-size: 10px; text-align: center; border-bottom: 1px solid grey; font-family: sans-serif; } div.glyph { display: block; position: relative; padding-top: 4px; width: 48px; font-size: 24px; text-align: center; } </style> </head> <body> <div id="element-fp"><span id="glyphs-span" style="font-size: 22000px;"><span id="glyphs-slot"></span></span></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <col width="15%"><col width="85%"> <thead><tr><th colspan="2"> <div class="nav-title">font async <div class="nav-up"><span class="c perf" id="locale"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"><span class="no_color"> Testing <a target="_blank" class="blue" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1676966#c54">async font fallback</a>. You need a new browser session and no history.<br> <div class="mono">DISPLAY: <input type="radio" id="family" name="family" value="cursive" onClick='rebuild()'><label> cursive </label> <input type="radio" id="family" name="family" value="math" onClick='rebuild()'><label> math </label> <input type="radio" id="family" name="family" value="monospace" onClick='rebuild()'><label> monospace </label> <input type="radio" id="family" name="family" value="sans-serif" onClick='rebuild()' checked><label> sans-serif </label> <input type="radio" id="family" name="family" value="serif" onClick='rebuild()'><label> serif </label> <input type="radio" id="family" name="family" value="system-ui" onClick='rebuild()'><label> system-ui </label> </div> </td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2" style="text-align: left;"> <span class="no_color" id="glyphs"></span> </td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2" style="text-align: left;"> <br>SUMMARY: &nbsp; <span class="no_color c mono spaces" id="results"></span> <br> </td></tr> <tr><td colspan="2" style="text-align: left;"> <br>DETAILS: &nbsp; <span class="btn0 btnc" onclick="copyclip(`details`)">[COPY]</span> <br><br><span class="no_color c mono spaces" id="details"></span> </td></tr> </table> <br> <script> 'use strict'; let fntCodes = [ // added 0x6E2F // sorted '0x007F','0x0218','0x058F','0x05C6','0x061C','0x0700','0x08E4','0x097F','0x09B3', '0x0B82','0x0D02','0x10A0','0x115A','0x17DD','0x1950','0x1C50','0x1CDA','0x1D790', '0x1E9E','0x20B0','0x20B8','0x20B9','0x20BA','0x20BD','0x20E3','0x21E4','0x23AE', '0x2425','0x2581','0x2619','0x2B06','0x2C7B','0x302E','0x3095','0x532D','0x6E2F', '0xA73D','0xA830','0xF003','0xF810','0xFBEE', /* ignore replacement characters: https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character these seem to be problematic at least on windows on first use '0xFFF9','0xFFFD', //*/ '0xFFFF', // tofu ] let styles = ["cursive","math","monospace","sans-serif","serif","system-ui"] let oData = {}, counter = 0 let div = dom['element-fp'], span = dom['glyphs-span'], slot = dom['glyphs-slot'] function build(style) { let tmpArray = [] fntCodes.sort() fntCodes.forEach(function(code) { let id = code.slice(2) let string = "<span class='box " + style +"' id='b"+ id +"'><span class='info'>"+ id +"</span><div class='glyph'><span>&#x"+ id +";</span></div></span>\n" tmpArray.push(string) }) //console.log(tmpArray[0]) dom.glyphs.innerHTML = tmpArray.join("") } function get_locale() { let locale try { locale = Intl.DateTimeFormat().resolvedOptions().locale } catch(e) { locale = zErr } dom.locale.innerHTML = locale } get_locale() function rebuild() { let style = document.querySelector('input[name="family"]:checked').value // build so everything is clear build(style) // color stuff if (isColor) { let data = oData.displayChanges[style] for (const k of Object.keys(data.text)) { dom["b"+k].classList.add(data.text[k]) } data.bg.forEach(function(id) { dom[id].classList.add('changed') }) data.zero.forEach(function(id) { let style = (data.bg.includes("b"+id)) ? "changedzero" : "zero" dom["b"+id].classList.add(style) }) } } function display(name) { let data = oData[name] let hash = mini(data) if (name == "1data") {name = "1st test" } else if (name == "2data") {name = "2nd test" } else if (name == "sizeChanges") {name = "differences"} dom.details.innerHTML = name +": "+ hash +"<br>"+ json_highlight(data, 130) } function output() { // analyse oData["2tofu"] = {} oData["sizeChanges"] = {} oData["displayChanges"] = {} oData["groupChanges"] = {} for (const style of Object.keys(oData["2data"])) { oData["sizeChanges"][style] = {} oData["groupChanges"][style] = {1:[], 2:[], 3:[], 4:[]} oData["displayChanges"][style] = { "bg": [], "text": {}, "zero": [] } let tofuSize = oData["tofuSize"][style] // match any tofu sizes for (const code of Object.keys(oData["2data"][style])) { // tofu size info let run2 = oData["2data"][style][code] let run2String = run2.join(" x ") let isTofu = run2String == tofuSize if (isTofu) { if (oData["2tofu"][style] == undefined) {oData["2tofu"][style] = []} oData["2tofu"][style].push(code) } // all size changes let id = code.slice(2) let run1 = oData["1data"][style][code] let run1String = run1.join(" x ") let isChanged = run1String !== run2String if (isChanged) { let firstchange = "n/a" oData["displayChanges"][style]["bg"].push("b"+id) // non-tofu items that change are not recorded try { firstchange = Math.round(oData["3data"][style][code][run2String]) } catch(e) {} oData["sizeChanges"][style][code] = [run1String, run2String, firstchange] } let state = oData["1tofu"][style].includes(code) * 1 state += ""+(isTofu) * 1 if (state == "00") { // not tofu both times if (isChanged) { oData["displayChanges"][style]["text"][id] = 'bad' oData["groupChanges"][style][3].push(code) } } else if (state == "11") { // tofu both times oData["displayChanges"][style]["text"][id] = 'good' oData["groupChanges"][style][1].push(code) } else if (state == "10") { // was tofu then fellback oData["displayChanges"][style]["text"][id] = 's12' oData["groupChanges"][style][2].push(code) } else { // last state can only be 01 // wasn't tofu but then was // could be possible that a fallback legit font == tofu size oData["displayChanges"][style]["text"][id] = 's3' oData["groupChanges"][style][4].push(code) } // run2: zero width or height if (run2[0] === 0 || run2[1] === 0) { oData["displayChanges"][style]["zero"].push(id) } } } /* test: mixed gradients: changed AND final zero-width oData["displayChanges"]["sans-serif"]["bg"].push('b097F') oData["displayChanges"]["sans-serif"]["zero"].push('097F') //*/ console.log(oData) isColor = true rebuild() // output data // clickable links // diffs including _when_ they changed let display = "" let hash1 = mini(oData["1data"]) let hash2 = mini(oData["2data"]) // " <span class='btn4 btnc' onclick='display(`" + item + "`)'>[" + array.length +"]</span>" if (hash1 == hash2) { display ="both tests: <span class='btn12 btnc' onclick='display(`1data`)'>" + hash1 +"</span>" } else { display = "1st test: <span class='btn12 btnc' onclick='display(`1data`)'>" + hash1 +"</span> | " +"2nd test: <span class='btn12 btnc' onclick='display(`2data`)'>" + hash2 +"</span> | " +"<span class='btn12 btnc' onclick='display(`sizeChanges`)'>diffs</span>" } dom.results.innerHTML = display } const run = (type) => new Promise(resolve => { // just grab data try { let aCodes = fntCodes let key if (type == "3data") { counter++ } if (oData[type] == undefined) {oData[type] = {}} styles.forEach(function(style) { slot.style.fontFamily = style if (oData[type][style] == undefined) {oData[type][style] = {}} if (type == "3data") { aCodes = oData["1tofu"][style] //oData[type][style][key] = {} } aCodes.forEach(function(code) { let item = String.fromCodePoint(code) slot.textContent = item // use clientrect for precision let width = span.getBoundingClientRect().width, height = div.getBoundingClientRect().height if (type == "3data") { if (oData[type][style][code] == undefined) {oData[type][style][code] = {}} let key = width +" x "+ height if (oData[type][style][code][key] == undefined) { oData[type][style][code][key] = performance.now() } } else { oData[type][style][code] = [width, height] } }) }) slot.textContent = "" if (type == "2data") { output() } return resolve(true) } catch(e) { slot.textContent = "" dom.results.innerHTML = e return resolve(e+"") } }) // run immediately, then after a delay let timer = 1500 let delay = 16 let isColor = false function runall() { // reset isColor = false dom.results = "" dom.details = "" rebuild() oData = {} oData = { "1start": performance.now() } counter = 0 // do it Promise.all([ run("1data") ]).then(function(results){ dom.results.innerHTML = sg + "1st test: completed" + sc + " ... 2nd test: will run in <span id='countdown'>2000</span> ms" let target = dom.countdown let t0 = performance.now() if (results[0]) { // build tofu list oData["tofuSize"] = {} oData["1tofu"] = {} for (const style of Object.keys(oData["1data"])) { // get tofu size PER style let array = oData["1data"][style]['0xFFFF'] let tofuSize = array.join(" x ") oData["tofuSize"][style] = tofuSize // match any tofu sizes for (const code of Object.keys(oData["1data"][style])) { array = oData["1data"][style][code] if (array.join(" x ") == tofuSize) { if (oData["1tofu"][style] == undefined) {oData["1tofu"][style] = []} oData["1tofu"][style].push(code) } } } function measure() { let time = (timer - Math.round(performance.now() - t0)) target.innerHTML = time if (time < delay) { clearInterval(loop) oData["2start"] = performance.now() dom.results.innerHTML = "" run("2data") } else { run("3data") } } let loop = setInterval(measure, delay) } else { dom.results.innerHTML = results[0] } }) } runall() </script> </body> </html> ================================================ FILE: tests/fontdebug.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=700"> <title>font debug</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 730px;} .visual { color: #b3b3b3; font-size: 96px !important; font-style: normal !important; letter-spacing: normal !important; line-break: auto !important; line-height: 50% !important; text-transform: none !important; text-align: left !important; /* this is just a visual: we already strip out spaces and I want a gap between unstyled and styled white-space: normal !important; */ word-break: normal !important; word-spacing: normal !important; } </style> </head> <body> <div class="hidden"><input type="reset" value="" id="widget0"></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <col width="20%"><col width="80%"> <thead><tr><th colspan="2"> <div class="nav-title">font debug <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">font </span> <div class="ttip"><span class="icon" style="font-size: 1.2em;">[ i ]</span> <span class="ttxt"> <b><u>examples</u><br><code>caption</code><br><code>-moz-info</code><br><code>arial</code><br> <code>MS Shell Dlg \32</code></b> </span> </div> &nbsp; <input id="valueF" type="text" style="width: 180px;" value="arial"> &nbsp; <span class="no_color"> text </span> &nbsp; <input id="valueT" type="text" style="width: 80px;"> &nbsp; <select name="weight" id="weight" style="width: 190px;"><option></option></select> &nbsp; <select name="styles" id="styles" style="width: 100px;"><option></option></select> </td></tr> <tr><td colspan="2" class="mono" style="text-align: left; vertical-align: top;"> <span class="btn12 btnfirst" onClick="run('family')">[ font-family ]</span> <span class="btn12 btnfirst" onClick="run('face')">[ FontFace ]</span> <span class="spaces faint">some characters: M ō á Ω - ? &#xFFFF;</span> <hr> <br> <div class="spaces" style="color: #b3b3b3;" id="base"></div> <div class="spaces" style="color: #b3b3b3;" id="info"></div> <div class="spaces visual" id="visual"></div> <div class="spaces" style="color: #b3b3b3;" id="font"></div> <div class="spaces" style="color: #b3b3b3;" id="detail"></div> </td> </tr> </table> <br> <script> 'use strict'; let fntList = [], tofu = '\uffff', baseFonts = ['monospace','sans-serif','serif','system-ui','cursive','fangsong'], ctrlFonts = ['monospace','sans-serif','serif'], fntString = "Mō-"+ tofu, fntStringUsed, fntStyle, fntWeight, fntSize = "512px", fntEverDetected = false, fntTestType = 'family' // default let aFaces = [] // history let fntSystem = ['caption','icon','menu','message-box','small-caption','status-bar', '-moz-window', '-moz-desktop', '-moz-document', '-moz-workspace', '-moz-info', '-moz-pull-down-menu', '-moz-dialog', '-moz-button', '-moz-list', '-moz-field', ] let fntFamilies = [ 'monospace','sans-serif','serif','system-ui','cursive','fangsong', 'emoji','math','ui-rounded','ui-monospsce','ui-sans-serif','ui-serif', ] function get_fontFace(font) { try { async function testLocalFontFamily(font) { try { const fontFace = new FontFace(font, `local("${font}")`) await fontFace.load() return font } catch(e) { return e+'' } } Promise.all([ testLocalFontFamily(font), ]).then(function(res){ let item = res[0], isDetected = true, strErr = '' if (font !== item) { isDetected = false if ('NetworkError: A network error occurred.' !== item) {strErr = item} } let str = isDetected ? green_tick : red_cross aFaces.push(str +' '+ font +' '+ strErr) dom.base.innerHTML = s12 + 'FONT FACE: results history'+ sc +'<br><br>'+ aFaces.join('<br>') }) } catch(e) { dom.base = e+'' } } function get_fontFamily(font) { //https://developer.mozilla.org/en-US/docs/Web/CSS/font dom.visual.style.font = '' dom.visual.style.setProperty('--font', '') if (fntSystem.includes(font)) { dom.visual.style.font = font } else { // if a generic font family, we don't wrap in quotes let fontString = fntFamilies.includes(font) ? font : '\''+ font + '\'' dom.visual.style.fontFamily = fontString } dom.info.innerHTML = '<br><hr>unstyled | styled | '+ font dom.visual.innerHTML = "<br><span>"+ fntStringUsed +" </span><span style ='font-weight: " + fntWeight +"; font-style:" + fntStyle + ";'>"+ fntStringUsed +"</span>" dom.valueF.value = font fntList = [font] getFonts() let strDetected = (fntEverDetected ? sg +' [' : sb +' [NO ') + 'CHANGE DETECTED]' + sc dom.font.innerHTML = '<br><br><hr><br>'+ s12 +'FONT: '+ sc + font +' [font-weight: '+ s3 + fntWeight + sc +' | font-style: ' + s3 + fntStyle + sc +'] '+ strDetected } function run(type) { // if enter key use last test type if ('enter' == type) {type = fntTestType} // set fntStringUsed if ((dom.valueT.value).trim() == '') { fntStringUsed = fntString } else { fntStringUsed = (dom.valueT.value).trim() } // replace multiple spaces //fntStringUsed = fntStringUsed.replace(/\s\s+/g, ' '); // remove all spaces fntStringUsed = fntStringUsed.replace(/ /g,'') dom.valueT.value = fntStringUsed let isStillFace = false, delay = 150 if ('face' == type && type == fntTestType) { isStillFace = true, delay = 0 } fntTestType = type // reset fntList = [] dom.font = '' dom.info = '' if (isStillFace) {dom.base = ''} dom.visual = '' dom.detail = '' fntEverDetected = false fntWeight = dom.weight.value fntStyle = dom.styles.value // make sure we have a font let valueF = dom.valueF.value valueF = valueF.replace(/['"]+/g, "") // remove all quote marks valueF = valueF.trim() if (valueF.length) { // only get the first font if multiple let tmpArr = valueF.split(",") for (let i = 0 ; i < tmpArr.length; i++) { let trimmed = tmpArr[i].trim() if (trimmed.length) { fntList.push(trimmed) } } // make sure we have at least one item fntList = fntList.filter(function (item, position) { return fntList.indexOf(item) === position }) if (!fntList.length) { dom.base = 'aww, snap! try adding a font' return } else { let font = fntList[0] setTimeout(function() { if ('family' == type) { get_fontFamily(font) } else { get_fontFace(font) } }, delay) } } else { dom.base = 'aww, snap! try adding a font' return } } function run_once() { // populate font weights let fntWeights = { 100: 'Thin (Hairline)', 200: 'Extra/Ultra Light', 300: 'Light', 400: 'Normal (Regular)', 500: 'Medium', 600: 'Semi/Demi Bold', 700: 'Bold', 800: 'Extra/Ultra Bold', 900: 'Black (Heavy)', } let aWeights = [] for (const k of Object.keys(fntWeights)) { aWeights.push("<option value = '"+ k +"'>"+ k +': '+ fntWeights[k] +"</option>") } dom.weight.innerHTML = aWeights.join('') dom.weight.value = '400' // populate fnt styles let aStyles = [] let fntStyles = ['italic','normal','oblique'] for (const k of fntStyles) { aStyles.push("<option value = '"+ k +"'>"+ k +"</option>") } dom.styles.innerHTML = aStyles.join('') dom.styles.value = 'normal' // tweak fntString to semi match what we get on TZP // FF windows: MōΩ + tofu // FF mac: Mō- + tofu // linux/android/TB: - + tofu <- boring/hard to inspect visually so instead just use mac's string // add platform specific fonts try { if ('Win32' == navigator.platform) { fntString = 'MōΩ'+ tofu baseFonts.push('MS Shell Dlg \\32') } } catch(e) {} try { let ua = navigator.userAgent if (ua.includes('Macintosh') || ua.includes('Mac OS')) { baseFonts.push('-apple-system') } else if (ua.includes('Android')) { baseFonts.push('Dancing Script') } } catch(e) {} // set string if ((dom.valueT.value).trim() == '') {dom.valueT.value = fntString} // another platform specific font, dedupe, sort let el = dom.widget0 try { let font = getComputedStyle(el).getPropertyValue("font-family") baseFonts.push(font.trim()) baseFonts = baseFonts.filter(function(item, position) {return baseFonts.indexOf(item) === position}) baseFonts.sort() } catch(e) { } // add enter key event to font field dom.valueF.addEventListener("keypress", function(event) {if (event.key === "Enter") {run('enter')}}) } run_once() function getFonts() { const id = 'element-fp' try { const doc = document const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) set_element(id) const span = doc.getElementById(`${id}-detector`) const style = getComputedStyle(span) const pixelsToNumber = pixels => +pixels.replace('px','') const originPixelsToNumber = pixels => 2*pixels.replace('px', '') const getDimensions = (span, style) => { const dimensions = { width: span.getBoundingClientRect().width, height: span.getBoundingClientRect().height, } return dimensions } // base sizes let baseDisplay = {} let fntStringBase = fntStringUsed //.slice(0, 12) baseFonts.sort() const base = baseFonts.reduce((acc, font) => { span.style.font ='' span.style.setProperty('--font', font) // if a generic font family, we don't wrap in quotes let fontString = fntFamilies.includes(font) ? font : '\''+ font + '\'' baseDisplay[font] = '<span style="white-space: nowrap; font-size: 24px; font-weight: ' + fntWeight + '; font-style: ' + fntStyle + '; font-family: '+ fontString + ';">'+ fntStringBase +'<span>' const dimensions = getDimensions(span, style) acc[font.split(',')[0]] = dimensions // use only first name, i.e w/o fallback return acc }, {}) span.style.font ='' // reset // display base let display = [] for (const k of Object.keys(base).sort()) { display.push('<p>'+ s3 + k.padStart(20) +': '+ sc + (base[k].width +'').padStart(20) + ' x '+ base[k].height + ' &nbsp; '+ baseDisplay[k] +'</p>' ) } let hash = mini(base) dom.base.innerHTML = s12+ 'BASEFONTS: '+ sc + hash + display.join('') // measure let results = {} fntList.forEach(font => { // we're only testing a single font, we don't need a font key ctrlFonts.forEach(basefont => { if (fntSystem.includes(font)) { span.style.setProperty('--font', "") span.style.font = font } else { // if a generic font family, we don't wrap in quotes let baseString = fntFamilies.includes(basefont) ? basefont : '\''+ basefont + '\'' const family = "'"+ font +"', "+ basefont span.style.font = "" span.style.setProperty('--font', family) } const style = getComputedStyle(span) const dimensions = getDimensions(span, style) basefont = basefont.split(",")[0] // switch to short generic name results[basefont] = {'width': dimensions.width, 'height': dimensions.height} return }) }) removeElementFn(id) // DETAIL //console.debug('base', mini(base), base) //console.debug('results', mini(results), results) display = [] for (const k of Object.keys(results).sort()) { let aCtrl = base[k], aTest = results[k] let isDetected = mini(aCtrl) !== mini(aTest) let strResult = (aTest.width+'').padStart(20) +' x ' + aTest.height if (isDetected) { strResult = sg + strResult + sc fntEverDetected = true } display.push('<p>'+ s3 + k.padStart(20) +": "+ sc + strResult +'</p>') } dom.detail.innerHTML = '<br>' + s12 +'DETAIL: '+ sc + mini(results) + display.join('') } catch(e) { removeElementFn(id) console.debug(e.name, e.message) } } function set_element(id) { document.getElementById(id).innerHTML = ` <style> #${id}-detector { --font: ''; position: absolute !important; left: -9999px!important; font-size: ` + fntSize + ` !important; font-style: ` + fntStyle + ` !important; font-weight: ` + fntWeight + ` !important; letter-spacing: normal !important; line-break: auto !important; line-height: normal !important; text-transform: none !important; text-align: left !important; text-decoration: none !important; text-shadow: none !important; white-space: normal !important; word-break: normal !important; word-spacing: normal !important; /* in order to test scrollWidth, clientWidth, etc. */ padding: 0 !important; margin: 0 !important; /* in order to test inlineSize and blockSize */ writing-mode: horizontal-tb !important; /* for transform and perspective */ transform-origin: unset !important; perspective-origin: unset !important; } #${id}-detector::after { font-family: var(--font); content: '` + fntStringUsed + `'; } </style> <span id="${id}-detector"></span>` } </script> </body> </html> ================================================ FILE: tests/fontdefaults.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>script defaults</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} </style> </head> <body> <div class="offscreen"> <div class="normalized"><span id="dfsize"></span></div> <div><span id="dfproportion"></span></div> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <thead><tr><th> <div class="nav-title">script defaults</div> </th></tr></thead> <tr><td class="intro"><span class="no_color"> Testing default proportional font-family, and sizes per <a target="_blank" class="blue" href="https://en.wikipedia.org/wiki/Writing_system">writing system</a> as per <code>Settings</code> > <code>General</code> > <code> Language and Appearance</code> > <code> Fonts</code> > <code> Advanced</code> </span></td></tr> <tr><td><hr><br></td></tr> <tr> <td class="mono" style="text-align: left"> <span class="spaces no_color" id="results"></span> </td> </tr> </table> <br> <script> 'use strict'; function run() { const styles = ["monospace","sans-serif","serif"] const scripts = { arabic: "ar", armenian: "hy", bengali: "bn", cyrillic: "ru", devanagari: "hi", ethiopic: "gez", georgian: "ka", greek: "el", gujurati: "gu", gurmukhi: "pa", hebrew: "he", japanese: "ja", kannada: "kn", khmer: "km", korean: "ko", latin: "en", malayalam: "ml", mathematics: "x-math", odia: "or", other: "my", "simplified chinese": "zh-CN", sinhala: "si", tamil: "ta", telugu: "te", thai: "th", tibetan: "bo","traditional chinese (hong kong)": "zh-HK", "traditional chinese (taiwan)": "zh-TW","unified canadian syllabary": "cr", } try { const el = dom.dfsize, elpro = dom.dfproportion let data = {} for (const k of Object.keys(scripts)) { let lang = scripts[k] elpro.style.fontFamily = "" elpro.setAttribute('lang', lang) let font = getComputedStyle(elpro).getPropertyValue("font-family") let tmp = [font] el.setAttribute('lang', lang) styles.forEach(function(style) { // always clear el.style.fontSize = "" el.removeAttribute('font-family') el.style.fontFamily = "" el.style.fontFamily = style let size = getComputedStyle(el).getPropertyValue("font-size").slice(0,-2) tmp.push(size) }) let key = tmp.join("-") if (data[key] == undefined) {data[key] = [k]} else {data[key].push(k)} } let newobj = {} for (const k of Object.keys(data).sort()) {newobj[k] = data[k]} // sort obj let hash = mini(newobj) dom.results.innerHTML = s12+ hash + sc +" <span class='btn0 btnc' onclick='copyclip(`results`)'>[COPY]</span><br><br>"+ json_highlight(newobj) } catch(e) { dom.results.innerHTML = s12 + e.name +": "+ sc + e.message } } run() </script> </body> </html> ================================================ FILE: tests/fontscripts.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>scripts</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 950px} #ugSpan {font-size: 10000%;} a.sb {color: #dc8c8c;} .groupleft {float: left;} .groupright {float: right;} .script {line-height: 2.4; font-family: monospace, "Courier New";} nav { position: absolute; padding: 15px; background-color: #161b22; } nav div { white-space: nowrap; margin-bottom: 12px; } div.nav-down {width: 300px;} div.nav-up {width: 300px;} code.purple { background-color: rgba(150, 100, 240, 0.4) !important; padding-left: 2px !important; padding-right: 2px !important; color: #dcdcdc; font-size: 11px; } div.chars { direction:ltr; unicode-bidi:bidi-override; } li { margin-left: 20px; } </style> </head> <body> <div class="offscreen"> <div id="ugDiv" class="normalized" style='font-size: 22pt;'><span id="ugSpan"><span id="ugSlot"></span></span></div> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <col width="25%"><col width="75%"> <thead><tr><th colspan="2"> <div class="nav-title">scripts <div class="nav-up"><span class="c perf" id="perf">waiting on reflow ...</span></div> <div class="nav-down"><span class="c perf" id="tofuSize"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Tests font vis vs script support by checking up to <code>x</code> characters per script for matching tofu sizes. Allow 2-3 secs before testing to ensure <a target="_blank" class="blue" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1676966#c54">asynchronous font fallback</a>. Tofu results are colored coded as <span class="bad">80+%</span>, <span class="s2">20-80%</span>, <span class="s4">20% or lower</span>. NOTE: <span class="s14">[RANGES...]</span> used can be <code class="purple">P</code> partial and/or <code class="purple">S</code> selective [uses some blocks in that range]. TIP: Click script names in the display to logs code points to console. </span> </td></tr> <tr> <td style="text-align: left; vertical-align: bottom;"> <span id="toggleScript" class="btn12 btnfirst" onClick="toggleitem(`legend`)">&#9660 index</span> <span id="countScript"></span> </td> <td style="text-align: left; vertical-align: bottom;"> <span class="btn12 btnfirst" onClick="run('test')">[ run ]</span> <span class="btn12 btn" onClick="toggleitem(`navmenu`)" style="position: relative;"> [ list &#9660; ] <nav id="navmenu" class="hidden" style="cursor: auto;"> <span class="no_color" style="font-size: 11px;">logs to console<br><br></span> <span style="cursor: pointer;"> <div onClick="logConsole('rangesreduced')">ranges [reduced]</div> <div onClick="logConsole('ranges')">ranges [used]</div> <div onClick="logConsole('reserved')">code points [reserved]</div> <div onClick="logConsole('ignore')">code points [ignored]</div> <div onClick="logConsole('max')">code points [max used]</div> <div onClick="logConsole('reduce')">code points [reduce]</div> <div onClick="logConsole('min')">code points [min used]</div> <div onClick="logConsole('version')">unicode [min versions]</div> <div onClick="logConsole('wiki')">wiki links</div> <div onClick="logConsole('everything')">everything</div> </span> </nav> </span> <span class="s12 mono" onClick='reset(true, false)'> depth <input type="radio" name="depth" value=20 checked> 20 <input type="radio" name="depth" value=50> 50 <input type="radio" name="depth" value="ALL"> ALL </span> | &nbsp; <span class="s12 mono"> <input type="checkbox" name="allassigned" style="margin: 0; height: 12px" onClick='reset()'> max <div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb" style="font-style: normal; font-family: serif;"> <br>When unchecked, some scripts<br>will use earlier unicode versions<br> <br>e.g. Armenian will use <b>v3.0</b> (1999) <br>ignoring <b>U+058F</b> (v6.1, 2012)<br><b>U+058D, U+058E</b> (v7.0, 2014)<br><b>U+0560, U+0588</b> (v11.0, 2018)<br> <br>Reduces tofu noise on platforms<br>with older font versions e.g. if<br>just checking basic script support<br><br></span> </div> </span> </td> </tr> <tr><td colspan="2"><hr></td></tr> <tr> <td style="text-align: left; vertical-align: top;"> <span class="mono spaces hidden" id="legend" style="color: #b3b3b3; font-size: 11px;"></span> <span class="mono spaces" id="summary" style="color: #b3b3b3; font-size: 11px;"></span> </td> <td style="text-align: left; vertical-align: top;"> <span class="spaces" style="direction:ltr;unicode-bidi:bidi-override; font-size: 12px;" id="visual"> <br>waiting on reflow ... </span> </td></tr> </table> <br> <script> 'use strict'; // https://en.wikipedia.org/wiki/List_of_Unicode_characters // https://www.cogsci.ed.ac.uk/~richard/unicode-sample-3-2.html let aPartial = [ // partial range: i.e start..end is not the full assigned range "unified canadian aboriginal syllabics", // 640 code points "arabic presentation forms-a", // 631 code points "basic latin", // C0 controls ignored "general punctuation", // 200x and 206x ignored, e.g. birectional, non-printing etc "latin-1 supplement", // 008x + 009x ignored, control codes "mathematical alphanumeric symbols", // 996 code points // partial: reduce entropy duplicity // i.e a lot of chars are the same measurements: east asian languages "cjk compatibility ideographs", "cjk compatibility ideographs supplement", "enclosed cjk letters and months", "hangul compatibility jamo", "hangul jamo", "hiragana", // partial! doh! way too big // also east asian measurments seem to all be the same per script "cjk unified ideographs", // 20,992 code points "cjk unified ideographs extension-a", // 6,592 code points "cjk unified ideographs extension-b", // 42,720 code points "hangul syllables", // 11,172 code points "yi syllables", // 1,165 code points ] let aSelective = [ // not all blocks in the range is used: i.e not "contiguous" // be careful to not exclude later assigned code points // or reduce size entropy per script "cjk compatibility ideographs", "cjk compatibility ideographs supplement", "enclosed cjk letters and months", "hangul jamo", "unified canadian aboriginal syllabics", ] let aSpacer = [ "combining diacritical marks", "combining diacritical marks for symbols", "combining diacritical marks supplement", "cyrillic extended-a", ] let oEnlarge = { "alphabetic presentation forms": [14], "arabic": [16], "arabic presentation forms-a": [16], "arabic supplement": [16], "arrows": [14], "bengali": [14], "buginese": [16], "buhid": [16], "bopomofo extended": [14], "cherokee supplement": [16], "combining diacritical marks": [20], "combining diacritical marks for symbmols": [20], "combining diacritical marks supplement": [20], "control pictures": [16], "cyrillic extended-a": [22], "deseret": [14], "devanagari": [14], "ethiopic": [14], "ethiopic supplement": [16], "general punctuation": [16], "glagolitic": [14], "hangul jamo": [14], "hebrew": [18], "ipa extensions": [14], "kanbun": [16], "katakana phonetic extensions": [16], "mahajani": [16], "mongolian": [16], "ogham": [18], "old turkic": [14], "optical character recognition": [18], "oriya": [14], "phonetic extensions": [16], "phonetic extensions supplement": [16], "runic": [14], "sinhala": [16], "sora sompeng": [16], "spacing modifier letters": [18], "superscripts and subscripts": [18], "supplemental arrows-a": [14], "supplemental arrows-b": [14], "syriac": [16], "tai le": [14], "telugu": [16], "thai": [14], "thanaa": [16], "tibetan": [18], "vertical forms": [16], "yi syllables": [16], } let uv1x0x0 = "1.0.0 (1991)", uv1x0x1 = "1.0.1 (1992)", uv1x1 = "1.1 (1993)", uv2x0 = "2.0 (1996)", uv3x0 = "3.0 (1999)", uv3x1 = "3.1 (2001)", uv3x2 = "3.2 (2002)", uv4x0 = "4.0 (2003)", uv4x1 = "4.1 (2005)", uv5x0 = "5.0 (2006)", uv5x1 = "5.1 (2008)", uv5x2 = "5.2 (2009)", uv6x1 = "6.1 (2012)", uv7x0 = "7.0 (2014)", uv8x0 = "8.0 (2015)", uv9x0 = "9.0 (2016)" let aBlocks = [ // note: first item needs to be a group name for the top anchor to work // schema: // name, prefix, [block range], remove last x, [reserved], url, [reduce], reduced-version, [ignore] // AFRICAN ["african"], ["adlam", "1E9", [0,1,2,3,4,5], 0, ["4C","4D","4E","4F","5A","5B","5C","5D"], "Adlam_(Unicode_block)", [ "4B", // 12.0 ], uv9x0 ], ["bamum", "A6", ["A","B","C","D","E","F"], -8, , "Bamum_(Unicode_block)"], ["bassa vah", "16A", ["D","E","F"], -10, ["EE","EF"], "Bassa_Vah_(Unicode_block)"], ["ethiopic", 1, [20,21,22,23,24,25,26,27,28,29,"2A","2B","2C","2D","2E","2F",30,31,32,33,34,35,36,37], -3, ["249","24E","24F","257","259","25E","25F","289","28E","28F","2B1","2B6","2B7","2BF", "2C1","2C6","2C7","2D7","311","316","317","35B","35C"], "Ethiopic_(Unicode_block)", [ "35D","35E", // 6.0 ], uv4x1 ], ["ethiopic supplement", "13", [8,9], -6, , "Ethiopic_Supplement"], //["medefaidrin", "16E", [4,5,6,7,8,9], -5, , "Medefaidrin_(Unicode_block)"], // noone supports this, reduce overhead/perf ["mende_kikakui", "1E8", [0,1,2,3,4,5,6,7,8,6,"A","B","C","D"], -9, ["C5","C6"], "Mende_Kikakui_(Unicode_block)"], ["nko", "07", ["C","D","E","F"], 0, ["FB","FC"], "NKo_(Unicode_block)", [ "FD","FE","FF", // 11.0 ], uv5x0 ], ["osmanya", "104", [8,9,"A"], -6, ["9E","9F"], "Osmanya_(Unicode_block)"], ["tifinagh", "2D", [3,4,5,6,7], 0, ["68","69","6A","6B","6C","6D","6E","71","72","73","74","75","76","77","78","79","7A","7B","7C","7D","7E"], "Tifinagh_(Unicode_block)", [ "70", // 6.0 "66","67", // 6.1 ], uv4x1, ["7F"] // ignore: tifinagh consonant joiner ], ["vai", "A", [50,51,52,53,54,55,56,57,58,59,"5A","5B","5C","5D","5E","5F",60,61,62,63], -20, , "Vai_(Unicode_block)"], // AMERICAN ["american"], ["cherokee", 13, ["A","B","C","D","E","F",], -2, ["F6","F7"], "Cherokee_(Unicode_block)", [ "F5","F8","F9","FA","FB","FC","FD", // 8.0 ], uv3x0 ], ["cherokee supplement", "AB", [7,8,9,"A","B"], , , "Cherokee_Supplement"], ["deseret", "104", [0,1,2,3,4], , , "Deseret_(Unicode_block)"], ["osage", "104", ["B","C","D","E","F"], -4, ["D4","D5","D6","D7"], "Osage_(Unicode_block)"], ["unified canadian aboriginal syllabics", 1, //[40,41,42,43,44,45,46,47,48,49,"4A","4B","4C","4D","4E","4F",50,51,52,53,54,55,56,57,58,59,"5A","5B","5C","5D","5E","5F",60,61,62,63,64,65,66,67], , , // full [40,41,42,43,44,45,67], , , // selective "Unified_Canadian_Aboriginal_Syllabics_(Unicode_block)", [ "400","677","678","679","67A","67B","67C","67D","67E","67F", // 5.2 ], uv3x0 ], // ANCIENT ["ancient and historic"], ["gothic", 103, [3,4], -5, , "Gothic_(Unicode_block)"], ["ogham", 16, [8,9], -3, , "Ogham_(Unicode_block)"], ["old italic", 103, [0,1,2], , ["24","25","26","27","28","29","2A","2B","2C"], "Old_Italic_(Unicode_block)", [ "1F", // 7.0 "2D","2E","2F" // 10.0 ], uv3x1 ], ["old turkic", "10C", [0,1,2,3,4], -7, , "Old_Turkic_(Unicode_block)"], ["runic", 16, ["A","B","C","D","E","F"], -7, , "Runic_(Unicode_block)", [ "F1","F2","F3","F4","F5","F6","F7","F8", // 7.0 ], uv3x0 ], // BRAHMIC ["brahmic"], ["balinese", "1B", [0,1,2,3,4,5,6,7], -1, ["4D","4E","4F"], "Balinese_(Unicode_block)", [ "4C","7D","7E" // 14.0 ], uv5x0 ], ["bengali", "09", [8,9,"A","B","C","D","E","F"], -1, ["84","8D","8E","91","92","A9","B1","B3","B4","B5","BA","BB","C5","C6","C9","CA","CF", "D0","D1","D2","D3","D4","D5","D6","D8","D9","DA","DB","DE","E4","E5"], "Bengali_(Unicode_block)", [ "FB", // 5.2 "80", // 7.0 "FC","FD", // 10.0 "FE", // 11.0 ], uv4x1 ], ["buginese", "1A", [0,1], 0, ["1C","1D"], "Buginese_(Unicode_block)"], ["buhid", 17, [4,5], -12, , "Buhid_(Unicode_block)"], ["chakma", 111, [0,1,2,3,4], -8, ["35"], "Chakma_(Unicode_block)", [ "44","45","46", // 11.0 "47" // 13.0 ], uv6x1 ], ["cham", "AA", [0,1,2,3,4,5], , ["37","38","39","3A","3B","3C","3D","3E","3F","4E","4F","5A","5B"], "Cham_(Unicode_block)"], ["common indic number forms", "A8", [3], -6, , "Common_Indic_Number_Forms"], ["devanagari", "09", [0,1,2,3,4,5,6,7], , , "Devanagari_(Unicode_block)", [ "71","72", // 5.1 "00","4E","55","79","7A", // 5.2 "3A","3B","4F","56","57","73","74","75","76","77", // 6.0 "78" // 7.0 ], uv5x0 ], ["dives akuru", 119, [0,1,2,3,4,5], -6, ["07","08","0A","0B","14","17","36","39","3A","47","48","49","4A","4B","4C","4D","4E","4F",], "Dives_Akuru_(Unicode_block)" ], ["dogra", 118, [0,1,2,3,4], -20, , "Dogra_(Unicode_block)"], ["gujarati", "0A", [8,9,"A","B","C","D","E","F"], 0, ["80","84","8E","92","A9","B1","B4","BA","BB","C6","CA","CE","CF", "D1","D2","D3","D4","D5","D6","D7","D8","D9","DA","DB","DC","DD","DE","DF", "E4","E5","F2","F3","F4","F5","F6","F7","F8"], "Gujarati_(Unicode_block)", [ "F0", // 6.1 "F9", // 8.0 "FA","FB","FC","FD","FE","FF", // 10.0 ], uv4x0 ], ["gurmukhi", "0A", [0,1,2,3,4,5,6,7], -9, ["00","04","0B","0C","0D","0E","11","12","29","31","34","37","3A","3B","3D", "43","44","45","46","49","4A","4E","4F","50","52","53","54","55","56","57","58", "5D","5F","60","61","62","63","64","65"], "Gurmukhi_(Unicode_block)", [ "51","75", // 5.1 "76", // 11.0 ], uv4x0 ], ["hanifi rohingya", "10D", [0,1,2,3], -6, ["28","29","2A","2B","2C","2D","2E","2F"], "Hanifi_Rohingya__(Unicode_block)"], ["hanunoo", 17, [2,3], -9, , "Hanunoo_(Unicode_block)"], ["javanese", "A9", [8,9,"A","B","C","D"], 0, ["CE","DA","DB","DC","DD"], "Javanese_(Unicode_block)"], ["kaithi", 110, [8,9,"A","B","C"], -2, ["C3","C4","C5","C6","C7","C8","C9","CA","CB","CC"], "Kaithi_(Unicode_block)", [ "CD", // 11.0 "C2", // 14.0 ], uv5x2 ], ["kannada", "0C", [8,9,"A","B","C","D","E","F"], -12, ["8D","91","A9","B4","BA","BB","C5","C9","CE","CF", "D0","D1","D2","D3","D4","D7","D8","D9","DA","DB","DC","DF","E4","E5","F0"], "Kannada_(Unicode_block)", [ "81", // 7.0 "80", // 9.0 "84", // 11.0 "DD", // 14.0 "F3", // 15.0 ], uv5x0 ], ["kawi", "11F", [0,1,2,3,4,5], -6, ["11","3B","3C","3D"], "Kawi_(Unicode_block)"], ["kayah li", "A9", [0,1,2], , , "Kayah_Li_(Unicode_block)"], ["khmer", 17, [8,9,"A","B","C","D","E","F"], -6, ["DE","DF","EA","EB","EC","ED","EE","EF"], "Khmer_(Unicode_block)", , , ["B4","B5"] // ignore: KIV AQ, KIV AA ], ["khmer symbols", 19, ["E","F"], , , "Khmer_Symbols"], ["khojki", 112, [0,1,2,3,4], -14, ["12"], "Khojki_(Unicode_block)", [ "3E", // 9.0 "3F","40","41", // 15.0 ], uv7x0 ], ["khudawadi", 112, ["B","C","D","E","F"], -6, ["EB","EC","ED","EE","EF"], "Khudawadi_(Unicode_block)"], ["lao", "0E", [8,9,"A","B","C","D","E","F"], -32, ["80","83","85","8B","A4","A6","BE","BF","C5","C7","CF","DA","DB"], "Lao_(Unicode_block)", [ "DE","DF", // 6.1 "86","89","8C","8E","8F","90","91","92","93","98","A0","A8","A9","AC","BA", // 12.0 "CE", // 15.0 ], uv1x0x1 ], ["lepcha", "1C", [0,1,2,3,4], , ["38","39","3A","4A","4B","4C"], "Lepcha_(Unicode_block)"], ["limbu", 19, [0,1,2,3,4], , ["1F","2C","2D","2E","2F","3C","3D","3E","3F","41","42","43"], "Limbu_(Unicode_block)", [ "1D","1E", // 7.0 ], uv4x0 ], ["mahajani", 111, [5,6,7], -9, , "Mahajani_(Unicode_block)"], ["malayalam", "0D", [0,1,2,3,4,5,6,7], 0, ["0D","11","45","49","50","51","52","53","64","65"], "Malayalam_(Unicode_block)", [ "29","3A","4E", // 6.0 "01", // 7.0 "5F", // 8.0 "4F","54","55","56","58","59","5A","5B","5C","5D","5E","76","77","78", // 9.0 "00","3B","3C", // 10.0 "04", // 13.0 ], uv5x1, ["4E"] // ignore: letter dot reph ], ["meetei mayek", "AB", ["C","D","E","F"], -6, ["EE","EF"], "Meetei_Mayek_(Unicode_block)"], ["modi", 116, [0,1,2,3,4,5], -6, ["45","46","47","48","49","4A","4B","4C","4D","4E","4F"], "Modi_(Unicode_block)"], ["mro", "16A", [4,5,6], , ["5F","6A","6B","6C","6D"], "Mro_(Unicode_block)"], ["multani", 112, [8,9,"A"], -6, ["87","89","8E","9E"], "Multani_(Unicode_block)"], ["myanmar", 10, [0,1,2,3,4,5,6,7,8,9], 0, , "Myanmar_(Unicode_block)", [ "9A","9B","9C","9D", // 5.2 ], uv5x1 ], ["myanmar extended-a", "AA", [6,7], , , "Myanmar_Extended-A", [ "7C","7D","7E","7F", // 7.0 ], uv5x2 ], ["myanmar extended-b", "A9", ["E","F"], -1, , "Myanmar_Extended-B"], ["nag mundari", "1E4", ["D","E","F"], -6, , "Nag_Mundari_(Unicode_block)"], ["new tai lue", 19, [8,9,"A","B","C","D"], 0, ["AC","AD","AE","AF","CA","CB","CC","CD","CE","CF","DB","DC","DD"], "New_Tai_Lue_(Unicode_block)", [ "AA","AB","DA", // 5.2 ], uv4x1 ], ["newa", 114, [0,1,2,3,4,5,6,7], -30, ["5C",], "Newa_(Unicode_block)", [ "5E", // 11.0 "5F", // 12.0 "5A","60","61", // 13.0 ], uv9x0 ], ["ol chiki", "1C", [5,6,7], , , "Ol_Chiki_(Unicode_block)"], ["oriya", "0B", [0,1,2,3,4,5,6,7], -8, ["00","04","0D","0E","11","12","29","31","34","3A","3B","45","46","49","4A","4E","4F", "50","51","52","53","54","58","59","5A","5B","5E","64","65"], "Oriya_(Unicode_block)", [ "72","73","74","75","76","77", // 6.0 "55", // 13.0 ], uv5x1 ], ["pau cin hau", "11A", ["C","D","E","F"], -7, , "Pau_Cin_Hau_(Unicode_block)"], ["phags-pa", "A8", [4,5,6,7], -8, , "Phags-pa_(Unicode_block)"], ["saurashtra", "A8", [8,9,"A","B","C","D"], -6, ["C6","C7","C8","C9","CA","CB","CC","CD"], "Saurashtra_(Unicode_block)", [ "C5", // 9.0 ], uv5x1 ], ["sinhala", "0D", [8,9,"A","B","C","D","E","F"], -11, ["80","84","97","98","99","B2","BC","BE","BF","C7","C8","C9","CB","CC","CD","CE","D5","D7", "E0","E1","E2","E3","E4","E5","F0","F1"], "Sinhala_(Unicode_block)", [ "E6","E7","E8","E9","EA","EB","EC","ED","EE","EF", // 7.0 "81", // 13.0 ], uv3x0 ], ["sora sompeng", 110, ["D","E","F"], -6, ["E9","EA","EB","EC","ED","EE","EF"], "Sora_Sompeng_(Unicode_block)"], ["sundanese", "1B", [8,9,"A","B"], , , "Sundanese_(Unicode_block)", [ "AB","AC","AD","BA","BB","BC","BD","BE","BF", // 6.1 ], uv5x1, ["AB"] // ignore: sign virama ], ["sundanese supplement", "1C", ["C"], -8, , "Sundanese_Supplement"], ["syloti nagri", "A8", [0,1,2], -3, , "Syloti_Nagri_(Unicode_block)", [ "2C", // 13.0 ], uv4x1 ], ["tagalog", 17, [0,1,], 0, ["16","17","18","19","1A","1B","1C","1D","1E"], "Tagalog_(Unicode_block)", [ "0D","15","1F" // 14.0 ], uv3x2 ], ["tagbanwa", 17, [6,7], -12, ["6D","71"], "Tagbanwa_(Unicode_block)"], ["tai le", 19, [5,6,7], -11, ["6E","6F"], "Tai_Le_(Unicode_block)"], ["tai tham", "1A", [2,3,4,5,6,7,8,9,"A"], -2, ["5F","7D","7E","8A","8B","8C","8D","8E","8F","9A","9B","9C","9D","9E","9F"], "Tai_Tham_(Unicode_block)" ], ["tai viet", "AA", [8,9,"A","B","C","D"], , ["C3","C4","C5","C6","C7","C8","C9","CA","CB","CC","CD","CE","CF","D0","D1","D2","D3","D4","D5","D6","D7","D8","D9","DA"], "Tai_Viet_(Unicode_block)" ], ["takri", 116, [8,9,"A","B","C"], -6, ["BA","BB","BC","BD","BE","BF"], "Takri_(Unicode_block)", [ "B8", // 12.0 "B9", // 14.0 ], uv6x1 ], ["tamil", "0B", [8,9,"A","B","C","D","E","F"], -5, ["80","81","84","8B","8C","8D","91","96","97","98","9B","9D", "A0","A1","A2","A5","A6","A7","AB","AC","AD","BA","BB","BC","BD", "C3","C4","C5","C9","CE","CF", "D1","D2","D3","D4","D5","D6","D8","D9","DA","DB","DC","DD","DE","DF", "E0","E1","E2","E3","E4","E5"], "Tamil_(Unicode_block)" ], ["tamil supplement", "11F", ["C","D","E","F"], , ["F2","F3","F4","F5","F6","F7","F8","F9","FA","FB","FC","FD","FE"], "Tamil_Supplement" ], ["telugu", "0C", [0,1,2,3,4,5,6,7], 0, ["0D","11","29","3A","3B","45","49","4E","4F", "50","51","52","53","54","57","5B","5C","5E","5F", "64","65","70","71","72","73","74","75","76"], "Telugu_(Unicode_block)", [ "00","34", // 7.0 "5A", // 8.0 "04", // 11.0 "77", // 12.0 "3C","5D", // 14.0 ], uv5x1 ], ["thai", "0E", [0,1,2,3,4,5,6,7], -36, ["00","3B","3C","3D","3E"], "Thai_(Unicode_block)"], ["tibetan", "0F", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], -37, ["48","6D","6E","6F","70","98","BD","CD"], "Tibetan_(Unicode_block)", [ "D0","D1", // 4.1 "6B","6C","CE","D2","D3","D4", // 5.1 "D5","D6","D7","D8", // 5.2 "8C","8D","8E","8F","D9","DA", // 6.0 ], uv3x0, ["0C"] // ignore: mark delimiter tsheg bstar ], ["tirhuta", 114, [8,9,"A","B","C","D"], -6, ["C8","C9","CA","CB","CC","CD","CE","CF"], "Tirhuta_(Unicode_block)"], ["warang citi", 118, ["A","B","C","D","E","F"], , ["F3","F4","F5","F6","F7","F8","F9","FA","FB","FC","FD","FE"], "Warang_Citi_(Unicode_block)"], // CYRILLIC ["cyrillic"], ["cyrillic", "04", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "Cyrillic_(Unicode_block)", [ "CF","FA","FB","FC","FD","FE","FF", // 5.0 "87", // 5.1 ], uv4x1 ], ["cyrillic extended-a", "2D", ["E","F"], , , "Cyrillic_Extended-A"], ["cyrillic extended-b", "A6", [4,5,6,7,8,9], , , "Cyrillic_Extended-B", [ "60","61", // 6.0 "74","75","76","77","78","79","7A","7B","9F", // 6.1 "98","99","9A","9B","9C","9D", // 7.0 "9E", // 8.0 ], uv5x1 ], ["cyrillic extended-c", "1C", [8], -7, , "Cyrillic_Extended-C"], ["cyrillic supplement", "05", [0,1,2], , , "Cyrillic_Supplement", [ "14","15","16","17","18","19","1A","1B","1C","1D","1E","1F","20","21","22","23", // 5.1 "24","25", // 5.2 "26","27", // 6.0 "28","29","2A","2B","2C","2D","2E","2F", // 7.0 ], uv5x0 ], ["glagolitic", "2C", [0,1,2,3,4,5], , , "Glagolitic_(Unicode_block)", [ "2F","5F" // 14.0 ], uv4x1 ], // EAST ASIAN ["east asian"], ["bopomofo", 31, [0,1,2], 0, ["00","01","02","03","04"], "Bopomofo_(Unicode_block)", [ "2D", // 5.1 "2E", // 10.0 "2F", // 11.0 ], uv1x0x0 ], ["bopomofo extended", 31, ["A","B"], , , "Bopomofo_Extended", [ "B8","B9","BA", // 6.0 "BB","BC","BD","BE","BF", // 13.0 ], uv3x0 ], ["cjk compatibility", 33, [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "CJK_Compatibility", [ "77","78","79","7A","DE","DF","FF", // 4.0 ], uv1x1 ], ["cjk compatibility forms", "FE", [3,4], , , "CJK_Compatibility_Forms"], ["cjk compatibility ideographs", "F", //[90,91,92,93,94,95,96,97,98,99,"9A","9B","9C","9D","9E","9F","A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","AA","AB","AC","AD","AE","AF"], -38, ["A6E","A6F"], // full [90,91,92,93,94,"A2","A6","AF"], -6, ["A6E","A6F"], // selective "CJK_Compatibility_Ideographs", [ "A6B","A6C","A6D", // 5.2 "A2E","A2F", // 6.1 ], uv4x1 ], ["cjk compatibility ideographs supplement", "2F", //[80,81,82,83,84,85,86,87,88,89,"8A","8B","8C","8D","8E","8F", 90,91,92,93,94,95,96,97,98,"9A","9B","9C","9D","9E","9F","A0","A1"], -2, , // full [82,83,84,87,88,89,"9B",], , , // selective "CJK_Compatibility_Ideographs_Supplement" ], ["cjk symbols and punctuation", 30, [0,1,2,3], , , "CJK_Symbols_and_Punctuation", , , ["00"] // ignore: ID SP ], ["cjk radicals supplement", "2E", [8,9,"A","B","C","D","E","F"], -12, ["9A"], "CJK_Radicals_Supplement"], ["cjk unified ideographs", "4E", [0,1,2,3], , , "CJK_Unified_Ideographs_(Unicode_block)"], ["cjk unified ideographs extension-a", 34, [0,1,2,3], , , "CJK_Unified_Ideographs_Extension_A"], ["cjk unified ideographs extension-b", 200, [0,1,2,3], , , "CJK_Unified_Ideographs_Extension_B"], ["enclosed cjk letters and months", "32", // [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , ["1F"], // full [1,2,4,5,7,"A","C","F"], , ["1F"], // selective "Enclosed_CJK_Letters_and_Months", [ "1D","1E","50","7C","7D","CC","CD","CE","CF", // 4.0 "7E", // 4.1 "44","45","46","47","48","49","4A","4B","4C","4D","4E","4F", // 5.2 "FF", // 12.1 ], uv3x2 ], ["hangul compatibility jamo", 31, // [3,4,5,6,7,8], , ["30"], "Hangul_Compatibility_Jamo", , , // full [3,4,5,6], , ["30","8F"], "Hangul_Compatibility_Jamo", , , // selective ["64"] // ignore: HF ], ["hangul jamo", 11, // [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , // full [5,"A","B","F"], , , // selective "Hangul_Jamo_(Unicode_block)", [ "5A","5B","5C","5D","5E","A3","A4","A5","A6","A7","FA","FB","FC","FD","FE","FF", // 5.2 ], uv1x1, ["5F","60"] // ignore: HCF hangul choseong filler, HJF hangul jungseong filler ], ["hangul syllables", "AC", [0,1,2,3], , , "Hangul_Syllables"], ["hiragana", 30, // [4,5,6,7,8,9], , // full [6,7,8,9], , // selective ["40","97","98"], "Hiragana_(Unicode_block)" ], ["ideographic description characters", "2F", ["F"], -4, , "Ideographic_Description_Characters_(Unicode_block)"], ["kanbun", 31, [9], , , "Kanbun_(Unicode_block)"], ["kangxi radicals", "2F", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D"], -10, , "Kangxi_Radicals_(Unicode_block)"], ["katakana", 30, ["A","B","C","D","E","F"], , , "Katakana_(Unicode_block)"], ["katakana phonetic extensions", 31, ["F"], , , "Katakana_Phonetic_Extensions"], ["lisu", "A4", ["D","E","F"], , , "Lisu_(Unicode_block)"], ["vertical forms", "FE", [1], -6, , "Vertical_Forms"], ["yi radicals", "A4", [9,"A","B","C"], -9, , "Yi_Radicals"], ["yi syllables", "A0", [0,1,2,3], , , "Yi_Syllables"], // GEORGIAN ["georgian"], ["georgian", 10, ["A","B","C","D","E","F"], , ["C6","C8","C9","CA","CB","CC","CE","CF"], "Georgian_(Unicode_block)", [ "C7","CD","FD","FE","FF", // 6.1 ], uv4x1 ], ["georgian extended", "1C", [9,"A","B"], , ["BB","BC"], "Georgian_Extended"], ["georgian supplement", "2D", [0,1,2], -2, ["26","28","29","2A","2B","2C"], "Georgian_Supplement", [ "27","2D" // 6.1 ], uv4x1 ], // GREEK ["greek"], ["greek and coptic", "03", [7,8,9,"A","B","C","D","E","F"], , ["78","79","80","81","82","83","8B","8D","A2"], "Greek_and_Coptic", [ "70","71","72","73","76","77","CF", // 5.1 "7F", // 7.0 ], uv5x0 ], ["greek extended", "1F", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], -1, ["16","17","1E","1F","46","47","4E","4F","58","5A","5C","5E","7E","7F","B5","C5","D4","D5","DC","F0","F1","F5"], "Greek_Extended" ], // LATIN ["latin"], ["basic latin", "00", [2,3,4,5,6,7], , , "Basic_Latin_(Unicode_block)", , , ["20","7F"] // ignore: SP, DEL ], ["latin-1 supplement", "00", ["A","B","C","D","E","F"], , , "Latin-1_Supplement", , , ["A0","AD"], // ignore: NBSP, SHY ], ["latin extended-a", "01", [0,1,2,3,4,5,6,7], , , "Latin_Extended-A"], ["latin extended-b", 0, [18,19,"1A","1B","1C","1D","1E","1F",20,21,22,23,24], , , "Latin_Extended-B"], ["latin extended-c", "2C", [6,7], , , "Latin_Extended-C"], ["latin extended-d", "A7", [2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , [ "CB","CC","CD","CE","CF","D2","D4","DA","DB","DC","DD","DE","DF", "E0","E1","E2","E3","E4","E5","E6","E7","E8","E9","EA","EB","EC","ED","EE","EF", "F0","F1" ], "Latin_Extended-D", [ "AE", // 9.0 "A7","B8","B9", // 11.0 "BA","BB","BC","BD","BE","BF","C2","C3","C4","C5","C6", // 12.0 "C7","C8","C9","CA","F5","F6", // 13.0 "C0","C1","D0","D1","D3","D5","D6","D7","D8","D9","F2","F3","F4", // 14.0 ], uv8x0 ], ["latin extended-e", "AB", [3,4,5,6], -4 , , "Latin_Extended-E", [ "66","67", // 12.0 "68","69","6A","6B", // 13.0 ], uv8x0 ], ["latin extended-f", 107, [8,9,"A","B"], -5 , ["86","B1"], "Latin_Extended-F"], ["latin extended-g", "1DF", [0,1,2], -5 , ["1F","20","21","22","23","24"], "Latin_Extended-G"], ["latin extended additional", "1E", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "Latin_Extended_Additional", [ "9C","9D","9E","9F","FA","FB","FC","FD","FE","FF", // 5.1 ], uv2x0 ], // PHONETIC ["phonetic"], ["ipa extensions", "02", [5,6,7,8,9,"A"], , , "IPA_Extensions"], ["phonetic extensions", "1D", [0,1,2,3,4,5,6,7], , , "Phonetic_Extensions"], ["phonetic extensions supplement", "1D", [8,9,"A","B"], , , "Phonetic_Extensions_Supplement"], ["spacing modifier letters", "02", ["B","C","D","E","F"], , , "Spacing_Modifier_Letters"], // SEMITIC ["semitic"], ["arabic", "06", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "Arabic_(Unicode_block)", [ "20","5F", // 6.0 "04", // 6.1 "1C", // 6.3 "05", // 7.0 "1D", // 14.0 ], uv5x1, ["1C"] // ALM ], ["arabic presentation forms-a", "FB", [5,6,7,8,9,"A"], , , "Arabic_Presentation_Forms-A"], ["arabic supplement", "07", [5,6,7], , , "Arabic_Supplement", [ "6E","6F","70","71","72","73","74","75","76","77","78","79","7A","7B","7C","7D","7E","7F", // 5.1 ], uv4x1 ], ["hebrew", "05", [9,"A","B","C","D","E","F"], -11, ["90","C8","C9","CA","CB","CC","CD","CE","CF","EB","EC","ED","EE"], "Hebrew_(Unicode_block)", [ "EF", // 11.0 ], uv5x0 ], ["mandaic", "08", [4,5], -1, ["5C","5D"], "Mandaic_(Unicode_block)"], ["samaritan", "08", [0,1,2,3], -1, ["2E","2F"], "Samaritan_(Unicode_block)"], ["syriac", "07", [0,1,2,3,4], 0, ["0E","4B","4C"], "Syriac_(Unicode_block)"], // symbols ["symbols"], ["alphabetic presentation forms", "FB", [0,1,2,3,4], , ["07","08","09","0A","0B","0C","0D","0E","0F","10","11","12","18","19","1A","1B","1C","37","3D","3F","42","45"], "Alphabetic_Presentation_Forms" ], ["arrows", 21, [9,"A","B","C","D","E","F"], , , "Arrows_(Unicode_block)"], ["block elements", 25, [8,9], , , "Block_Elements"], ["box drawing", 25, [0,1,2,3,4,5,6,7], , , "Box_Drawing"], ["braille patterns", 28, [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , ,"Braille_Patterns"], ["currency symbols", 20, ["A","B","C"], -15, , "Currency_Symbols_(Unicode_block)", [ "B6","B7","B8", // 5.2 "B9", // 6.0 "BA", // 6.2 "BB","BC","BD", // 7.0 "BE", // 8.0 "BF", // 10.0 "C0", // 14.0 ], uv4x1 ], ["dingbats", 27, [0,1,2,3,4,5,6,7,8,9,"A","B"], , , "Dingbat", [ "57", // 5.2 "05","0A","0B","28","4C","4E","53","54","55","5F","60","95","96","97","B0","BF", // 6.0 "00", // 7.0 ], uv3x2 ], ["enclosed alphanumerics", 24, [6,7,8,9,"A","B","C","D","E","F"], , , "Enclosed_Alphanumerics"], ["general punctuation", 20, [1,2,3,4,5], , , "General_Punctuation", , , [ "11", // NB "28","29", // L SEP, P SEP "2A","2B", // LRE, RLE "2C", // PDF "2D","2E", // LRO, RLO "2F", // NNB SP "5F", // MM SP ], ], ["geometric shapes", 25, ["A","B","C","D","E","F"], , , "Geometric_Shapes_(Unicode_block)"], ["letterlike symbols", 21, [0,1,2,3,4], , , "Letterlike_Symbols", [ "3C","4C", // 4.1 "4D","4E", // 5.0 "4F", // 5.1 ], uv4x0 ], ["mathematical alphanumeric symbols", "1D4", [0,1,2,3,4,5,6,7,8,9], , ["55","9D"], "Mathematical_Alphanumeric_Symbols" ], ["mathematical operators", 22, [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "Mathematical_Operators_(Unicode_block)" ], ["miscellaneous mathematical symbols-a", 27, ["C","D","E"], , , "Miscellaneous_Mathematical_Symbols-A", [ "C0","C1","C2","C3","C4","C5","C6", // 4.1 "CA","C7","C8","C9", // 5.0 "CC","EC","ED","EE","EF", // 5.1 "CE","CF", // 6.0 "CB","CD", // 6.1 ], uv3x2 ], ["miscellaneous mathematical symbols-b", 29, [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "Miscellaneous_Mathematical_Symbols-B" ], ["miscellaneous symbols", 26, [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "Miscellaneous_Symbols", [ "B2", // 5.0 "9D","B3","B4","B5","B6","B7","B8","B9","BA","BB","BC","C0","C1","C2","C3", // 5.1 "9E","9F","BD","BE","BF","C4","C5","C6","C7","C8","C9","CA","CB","CC","CD","CF", "D0","D1","D2","D3","D4","D5","D6","D7","D8","D9","DA","DB","DC","DD","DE","DF", "E0","E1","E3","E8","E9","EA","EB","EC","ED","EE","EF", "F0","F1","F2","F3","F4","F5","F6","F7","F8","F9","FA","FB","FC","FD","FE","FF", // 5.2 "CE","E2","E4","E5","E6","E7", // 6.0 ], uv4x1 ], ["miscellaneous technical", 23, [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , ["29","2A"], // 2 deprecated 5.2 "Miscellaneous_Technical", [ "DC","DD","DE","DF","E0","E1","E2","E3","E4","E5","E6","E7", // 5.0 "E8", // 5.2 "E9","EA","EB","EC","ED","EE","EF","F0","F1","F2","F3", // 6.0 "F4","F5","F6","F7","F8","F9","FA", // 7.0 "FB","FC","FD","FE", // 9.0 "FF", // 10.0 ], uv4x1 ], ["number forms", 21, [5,6,7,8], -4, , "Number_Forms", [ "85","86","87","88", // "50","51","52","89", // 5.2 "8A","8B", // 8.0 ], uv5x0 ], ["optical character recognition", 24, [4,5], -21, ,"Optical_Character_Recognition_(Unicode_block)"], ["superscripts and subscripts", 20, [7,8,9], -3, ["72","73","8F"], "Superscripts_and_Subscripts_(Unicode_block)", [ "95","96","97","98","99","9A","9B","9C", // 6.0 ], uv4x1 ], ["supplemental arrows-a", 27, ["F"], , ,"Supplemental_Arrows-A"], ["supplemental arrows-b", 29, [0,1,2,3,4,5,6,7], , ,"Supplemental_Arrows-B"], ["supplemental mathematical operators", "2A", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"], , , "Supplemental_Mathematical_Operators" ], // MISC ["misc"], ["armenian", "05", [3,4,5,6,7,8], , ["30","57","58","8B","8C"], "Armenian_(Unicode_block)", [ "8F", // 6.1 "8D","8E", // 7.0 "60","88", // 11.0 ], uv3x0 ], ["combining diacritical marks", "03", [0,1,2,3,4,5,6], , , "Combining_Diacritical_Marks", , , ["4F"] // ignore: CGJ ], ["combining diacritical marks for symbols", 20, ["D","E","F"], -15, , "Combining_Diacritical_Marks_for_Symbols", [ "E2","E3", // 3.0 "E4","E5","E6","E7","E8","E9","EA", // 3.2 "EB", // 4.1 "EC","ED","EE","EF", // 5.0 "F0", // 5.1 ], uv1x0x0 ], ["combining diacritical marks supplement", "1D", ["C","D","E","F"], , , "Combining_Diacritical_Marks_Supplement", [ "", // 5.1 "FD", // 5.2 "FC", // 6.0 "E7","E8","E9","EA","EB","EC","ED","EE","EF","F0","F1","F2","F3","F4","F5", // 7.0 "FB", // 9.0 "F6","F7","F8","F9", // 10.0 "FA", // 14.0 ], uv5x1 ], ["control pictures", 24, [0,1,2,3], -25, , "Control_Pictures"], ["halfwidth and fullwidth forms", "FF", [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E"], -1, ["00","BF","C0","C1","C8","C9","D0","D1","D8","D9","DD","DE","DF","E7"], "Halfwidth_and_Fullwidth_Forms_(Unicode_block)", , , ["A0"] // ignore: HW HF ], ["mongolian", 18, [0,1,2,3,4,5,6,7,8,9,"A"], -5, ["1A","1B","1C","1D","1E","1F","79","7A","7B","7C","7D","7E","7F"], "Mongolian_(Unicode_block)", [ "AA", // 5.1 "78", // 11.0 "05", // 14.0 ], uv3x0, ["0B","0C","0D","0E","0F"] // ignore: FVS1, FVS2, FVS3, MVS, FVS4 ], ["thanaa", "07", [8,9,"A","B"], -14, , "Thaana_(Unicode_block)"], ] let aBlocksSkip = [ // to easily turn scripts on/off // not for production as headers can remain and anchors get fucked up? // all 129 scripts are listed here in different buckets for easily // generating different everyCode lists /* everything not in the other list 'adlam', 'bamum', 'bassa vah', 'ethiopic', 'mende kikakui', 'nko', 'osmanya', 'tifinagh', 'vai', 'cherokee', 'cherokee supplement', 'chakma', 'deseret', 'osage', 'unified canadian aboriginal syllabics', 'old italic', 'balinese', 'bengali', 'buginese', 'buhid', 'cham', 'common indic number forms', 'devanagari', 'dives akuru', 'dogra', 'gujarati', 'gurmukhi', 'hanifi rohingya', 'hanunoo', 'javanese', 'kaithi', 'kannada', 'kawi', 'kayah li', 'khmer', 'khmer symbols', 'khojki', 'khudawadi', 'lao', 'lepcha', 'limbu', 'mahajani', 'malayalam', 'meetei mayek', 'modi', 'mro', 'multani', 'myanmar', 'myanmar extended-a', 'myanmar extended-b', 'nag mundari', 'new tai lue', 'newa', 'ol chiki', 'oriya', 'pau cin hau', 'phags-pa', 'saurashtra', 'sinhala', 'sora sompeng', 'sundanese', 'sundanese supplement', 'syloti nagri', 'tagalog', 'tagbanwa', 'tai le', 'tai tham', 'tai viet', 'takri', 'tamil', 'telugu', 'thai', 'tibetan', 'tirhuta', 'warang citi', 'cyrillic supplement', 'bopomofo', 'bopomofo extended', 'cjk compatibility', 'cjk compatibility forms', 'cjk symbols and punctuation', 'cjk radicals supplement', 'cjk unified ideographs', 'cjk unified ideographs extension-a', 'enclosed cjk letters and months', 'hangul compatibility jamo', 'hangul jamo', 'hangul syllables', 'hiragana', 'ideographic description characters', 'kanbun', 'katakana', 'katakana phonetic extensions', 'lisu', 'vertical forms', 'yi radicals', 'yi syllables', 'georgian', 'georgian extended', 'georgian supplement', 'greek and coptic', 'greek extended', 'armenian', 'halfwidth and fullwidth forms', 'mongolian', 'thanaa', //*/ /* list to reduce collection of all everyCode for fntString codepoints // list excludes some obvious crap like modifiers, combining, blocks, latin "ethiopic supplement", "gothic", "ogham", "old turkic", "runic", "tamil supplement", "cyrillic", "cyrillic extended-a", "cyrillic extended-b", "cyrillic extended-c", "glagolitic", "cjk compatibility ideographs", "cjk compatibility ideographs supplement", "cjk unified ideographs extension-b", "kangxi radicals", "basic latin", "latin-1 supplement", "latin extended-a", "latin extended-b", "latin extended-c", "latin extended-d", "latin extended-e", "latin extended-f", "latin extended-g", "latin extended additional", "ipa extensions", "phonetic extensions", "phonetic extensions supplement", "spacing modifier letters", "arabic", "arabic presentation forms-a", "arabic supplement", "hebrew", "mandaic", "samaritan", "syriac", "alphabetic presentation forms", "arrows", "block elements", "box drawing", "braille patterns", "currency symbols", "dingbats", "enclosed alphanumerics", "general punctuation", "geometric shapes", "letterlike symbols", "mathematical alphanumeric symbols", "mathematical operators", "miscellaneous mathematical symbols-a", "miscellaneous mathematical symbols-b", "miscellaneous symbols", "miscellaneous technical", "number forms", "optical character recognition", "superscripts and subscripts", "supplemental arrows-a", "supplemental arrows-b", "supplemental mathematical operators", "combining diacritical marks", "combining diacritical marks for symbols", "combining diacritical marks supplement", "control pictures", //*/ ] let aTest = [ // these are the "reduce" code points in some scripts // known scripts where if the additonal code point is not supported, subsequent code points may not render // lets test each in a div or new line against the first min set "unified canadian aboriginal syllabics", "bengali","devanagari","kannada","malayalam","sinhala","telugu", "miscellaneous mathematical symbols-a","superscripts and subscripts", // or use this for any small set tests // e.g. aTest = aSpacer ] let isTest = false let oCodesMin = {}, oCodesMinVersion = {}, oCodesMax = {}, oCodesReduce = {}, oCodesReserved = {}, oCodesIgnore = {}, // bidirectional, non-printing, control codes etc oCodesCounts = {}, oRanges = {}, oRangesReduced = {}, oWiki = {}, oGroupHeader = {}, oScriptHeader = {}, aLegendClean = [], aNav = [], aDisplay = [], aDisplayMax = [], oTofuSizes = {}, tofuSize, testParams = "" let ZWNJ = String.fromCodePoint("0x200C") +" " let styles = ["none","sans-serif","serif","monospace","cursive","fantasy"] // FP let fpTofuData = {}, fpTofuHashes = [], // size fpSizeDataClient = {}, // hashes fpSizeHashesClient = [], // unique sizes fpSizeClientUnique = [] // everything let oCodePoints = {}, oTempData = {} function strip(value) {return value.replace(/ /g,"")} // strip all spaces function hyperlink(string) {return " <a target='blank' class='blue' href='"+ string +"'>wiki</a>"} function toggleitem(item) { let el = document.getElementById(item) let style = el.getAttribute("class") if (style.includes("hidden")) { el.classList.remove("hidden") } else { el.classList.add("hidden") } } function get_count(prefix) { let name = (dom.allassigned.checked ? "max" : "min") + document.querySelector('input[name="depth"]:checked').value let value = oCodesCounts[name] let maxValue = oCodesCounts["maxALL"] return prefix + (name == "maxALL" ? "all " : "") + value + (name == "maxALL" ? "" : " of "+ maxValue) //+" max chars" } function build_title(name, anchor, nameColor, grouptitle, intCodes, intCodesMax) { // builds the html code for each script header, including if we need a script group let count = intCodes let strVersion = oTempData[name]["min version"] if (strVersion !== undefined) { if (intCodes == intCodesMax) { count += sb + " <sup>counts match</sup></span> | " + intCodesMax } else { count += " <span class='no_color'><sup>"+ strVersion +"</sup></span> | " + intCodesMax } } else if (intCodes !== intCodesMax) { count += sb + " <sup>version</sup>"+ sc +" | "+ intCodesMax } let strWiki = oTempData[name]["wiki"] strWiki = strWiki == undefined ? "" : hyperlink(strWiki) let strRange = oTempData[name]["range"] if (strRange == undefined) { strRange = sb +"[range]"+ sc } else { strRange = s14 +" ["+ strRange +"]"+ sc + (aPartial.includes(name) ? " <code class='purple'>P</code>" : "") + (aSelective.includes(name) ? " <code class='purple'>S</code>" : "") } return (grouptitle !== "" ? oGroupHeader[grouptitle][0] : "") + //"<span class='mono script'>" "<span class='script'>" // name + nameColor + "<a name='"+ anchor + "'></a>" + "<span style='cursor: pointer;' onClick='logConsole(`"+ name + "`)'>" + name + sc + sc // char count + s12 + " ["+ count +"]"+ sc + strRange + strWiki + sc } function get_tofu_size() { // measure some known unassigned + reserved unicode glyphs return new Promise(resolve => { // list: some reserved code points // use multiple scripts to reduce change in greatest occurence over time let list = [ '0x0402', // control: cyrillic: capital Dje '0x0386', // control: greek: capital A with acute '0x09E5', // bengali '0x135C', // ethiopic '0x10CF', // georgian '0x0AF8', // gujarati '0x0A65', // gurmukhi '0x03A2', // greek '0x0EDB', // lao '0x0D65', // malayam /* '0x187F', // mongolian '0x0DF1', // sinhala '0x0BE5', // tamil '0x2D7E', // tifinagh */ ] let div = dom.ugDiv, span = dom.ugSpan, slot = dom.ugSlot let res = {} styles.forEach(function(style) {res[style] = []}) oTofuSizes["test array"] = [] list.forEach(function(codepoint) { oTofuSizes["test array"].push(codepoint +" "+ String.fromCodePoint(codepoint)) styles.forEach(function(style) { slot.style.fontFamily = style slot.textContent = String.fromCodePoint(codepoint) // use clientrects (more precision): w=span, h=div let cDiv = div.getClientRects() let cSpan = span.getClientRects() let whClient = cSpan[0].width +" x "+ cDiv[0].height res[style].push(whClient) }) }) // reset slot so we don't end up with unwanted scrollbars slot.textContent = "" // get most common size const names = Object.keys(res) for (const k of names) { let aRes = res[k] let getGreatestOccurrence = aRes => aRes.reduce((greatest, currentValue, index, res) => { let count = res.filter(item => JSON.stringify(item) == JSON.stringify(currentValue)).length if (count > greatest.count) { return {count, item: currentValue} } return greatest }, { count: 0, item: undefined }) let greatest = getGreatestOccurrence(aRes) oTofuSizes[k] = [greatest.item, aRes.join(", ")] } tofuSize = oTofuSizes["none"][0] dom.tofuSize.innerHTML = "<span style='cursor: pointer;' onClick='logConsole(`tofusize`)'>" + tofuSize +"</span>" return resolve() }) } function prettyObjectArray(json) { if (typeof json === 'string') { json = JSON.parse(json); } let output = JSON.stringify(json, function(k,v) { if(v instanceof Array) return JSON.stringify(v); return v; }, 3).replace(/\\/g, '') .replace(/\"\[/g, '[') .replace(/\]\"/g,']') .replace(/\"\{/g, '{') .replace(/\}\"/g,'}') .replace(/,/g,', '); return output; } function logConsole(type, name) { // objects let oUsed = {}, logStr = "" if (type == "everything") {oUsed = oCodePoints; logStr = "everything" } else if (type == "wiki") {oUsed = oWiki; logStr = "wikipedia links" } else if (type == "ranges") {oUsed = oRanges; logStr = "ranges used [not all blocks may be used]" } else if (type == "rangesreduced") { oUsed = oRangesReduced logStr = "ranges [reduced] [P: partial | S: selective blocks used from the range]" } else if (type == "ignore") {oUsed = oCodesIgnore; logStr = "code points [ignored]" } else if (type == "max") {oUsed = oCodesMax; logStr = "code points [max used]" } else if (type == "min") {oUsed = oCodesMin; logStr = "code points [min used if reduced]" } else if (type == "reduce") {oUsed = oCodesReduce; logStr = "code points [reduce]" } else if (type == "reserved") {oUsed = oCodesReserved; logStr = "code points [reserved]" } else if (type == "version") {oUsed = oCodesMinVersion; logStr = "unicode [min version if reduced]" } else if (type == "fpTofuData") { oUsed = fpTofuData; logStr = "tofu | "+ testParams } else if (type == "fpSizeDataClient") { oUsed = fpSizeDataClient; logStr = "getClientRects | "+ testParams } else if (type == "fptofuscript") { oUsed = fpTofuData[name]; logStr="tofu ["+ name +"] | "+ testParams } if (logStr !== "") { console.log(logStr+"\n", prettyObjectArray(oUsed)) return } // combine if (type == "fpsizescript") { logStr="sizes ["+ name +"] | "+ testParams console.log( logStr+"\n"+ "\ngetClientRects\n", prettyObjectArray(fpSizeDataClient[name]) ) return } // arrays if (type == "fptofu") { logStr = "tofu fingerprint | "+ testParams +" | "+ mini(fpTofuHashes.join()) +"\n "+ fpTofuHashes.join("\n ") } else if (type == "fpSizeClient") { logStr = "getClientRects | "+ testParams +" | "+ mini(fpSizeHashesClient.join()) +"\n "+ fpSizeHashesClient.join("\n ") } if (logStr !== "") { console.log(logStr) return } // pretty if (type == "tofusize") { logStr = "calculated tofu sizes [greatest occurence, results]" logStr += "\n===========" const names = Object.keys(oTofuSizes).sort() for (const k of names) { logStr += "\n"+ k + (k == "test array" ? "\n - "+ oTofuSizes[k].join(", ") : "\n - "+ oTofuSizes[k][0]) + (k == "test array" ? "" : "\n - "+ oTofuSizes[k][1]) } console.log(logStr) return } // script if (oCodePoints[type] !== undefined) { console.log(type, prettyObjectArray(oCodePoints[type])) } } function generate() { // do once // two sets of codes and characters and clean displays // reduce codes/chars // reserved code/chars // ignore codes // range info // clean legend // get char counts return new Promise(resolve => { let t0 = performance.now() aBlocks.forEach(function(data) { let name = data[0].toLowerCase() let anchor = strip(name) +"group" if (data[1] == undefined) { aNav.push(anchor) } }) // FP lines let strFP = "<span class='mono'>" + s12 + "tofu size: ".padStart(16) + sc +"<span id='runningTofuSize'></span><br>" + s12 + "parameters: ".padStart(16) + sc +"<span id='runningParameters'></span><br>" + s12 +"tofu: ".padStart(16) + sc +"<br>" + s12 +"getClientRects: ".padStart(16) + sc + "<span id='runningStatus'></span></span><br>" aDisplay.push(strFP) aDisplayMax.push(strFP) let aSuffix = [0,1,2,3,4,5,6,7,8,9,"A","B","C","D","E","F"] let navIndex = 0, navString ="", navTop = "<a class='blue' href='#" + aBlocks[0][0].toLowerCase() +"group'> &#9650 TOP</a> &nbsp ", navEnd = "<a class='blue' href='#end'> &#9660 END</a></p>", grouptitle = "" let aCountsMin = [] let aCountsMax = [] let namesDebug = [] aBlocks.forEach(function(data) { let name = data[0].toLowerCase() let anchor = strip(name) if (data[1] == undefined) { // group headers name = name.toUpperCase() anchor += "group" navString = "<p class='groupright'>" if (anchor !== aNav[0]) { navString += navTop + "<a class='blue' href='#"+ aNav[navIndex - 1] +"'> &#9664 PREV</a> &nbsp " } if (aNav[navIndex + 1] !== undefined) { navString += "<a class='blue' href='#"+ aNav[navIndex + 1] +"'> &#9654 NEXT</a> &nbsp " } navString += navEnd aLegendClean.push((navIndex == 0 ? "": "<br>") +"<a class='blue' href='#"+ anchor +"'>"+ name +"</a><br>") navIndex++ let groupStr = "<div><a name='"+ anchor + "'></a>" + "<hr><p class='groupleft'>" + "<u><b><a class='no_color' href='#"+ anchor +"'>"+ name +"</a></b></u>" + "</p>" + navString + "<div style='clear: both;'></div>" + "</div>" grouptitle = strip(name).toLowerCase() oGroupHeader[grouptitle] = [groupStr] } else { name = data[0].toLowerCase() namesDebug.push(name) let go = !isTest if (aBlocksSkip.includes(name)) { go = false } else { go = true } if (isTest && aTest.includes(name)) {go = true} if (go) { oTempData[name] = {} let prefix = data[1], range = data[2], remove = data[3], nonassigned = data[4], url = data[5], reduce = data[6], version = data[7], ignore = data[8] // fixup vars if (nonassigned == undefined) {nonassigned = []} if (reduce == undefined) {reduce = []} if (ignore == undefined) {ignore = []} if (remove == undefined) {remove = 0} // range info let rangeString = ""+ prefix + range[0] +"0.."+ prefix + range[range.length-1] +"F" oTempData[name]["range"] = rangeString // partial let isPartial = [] if (aPartial.includes(name)) {isPartial.push("P")} if (aSelective.includes(name)) {isPartial.push("S")} oTempData[name]["partial"] = isPartial.length ? isPartial.join(", ") : false // legend aLegendClean.push("<li><a class='no_color' href='#"+ anchor +"'>"+ name +"</a></li></span>") let aCodesMin = [], aCodesMax = [], aCodesReduce = [], aCodesReserved = [], aCodesIgnore = [], aCharsMin = [], aCharsMax = [] // loop for (let i = 0; i < range.length; i++) { for (let j = 0; j < aSuffix.length; j++) { let string = "0x"+ prefix + range[i] + aSuffix[j] let match = ""+ range[i] + aSuffix[j] if (ignore.includes(match)) { aCodesIgnore.push(string) } else if (!nonassigned.includes(match)) { aCodesMax.push(string) aCharsMax.push(String.fromCodePoint(string)) if (!reduce.includes(match)) { aCodesMin.push(string) aCharsMin.push(String.fromCodePoint(string)) } else { aCodesReduce.push(string) } } else { aCodesReserved.push(string) } } } // trim results if (remove < 0) { // add trimmed items to reserved let trimReserved = aCodesMin.slice(aCodesMin.length + remove, aCodesMin.length) aCodesReserved = aCodesReserved.concat(trimReserved) // slice results aCodesMin = aCodesMin.slice(0, remove) aCodesMax = aCodesMax.slice(0, remove) aCharsMin = aCharsMin.slice(0, remove) aCharsMax = aCharsMax.slice(0, remove) } // record if (aCodesReserved.length) {oTempData[name]["reserved"] = aCodesReserved} if (aCodesIgnore.length) {oTempData[name]["ignored"] = aCodesIgnore} oTempData[name]["max count"] = aCodesMax.length oTempData[name]["max used"] = aCodesMax if (aCodesReduce.length) { oTempData[name]["reduce"] = aCodesReduce oTempData[name]["min count"] = aCodesMin.length oTempData[name]["min used"] = aCodesMin } if (version !== undefined && version !== "") {oTempData[name]["min version"] = version} if (url !== undefined && url !== "") {oTempData[name]["wiki"] = "https://en.wikipedia.org/wiki/"+ url} // counts aCountsMin.push(aCodesMin.length) aCountsMax.push(aCodesMax.length) // build clean displays let nameColor = s12 let scriptheader = build_title(name, anchor, nameColor, grouptitle, aCodesMin.length, aCodesMax.length) oScriptHeader[name] = [scriptheader] aDisplay.push(scriptheader) aDisplayMax.push(scriptheader) let strStart = "", strEnd = "" if (oEnlarge[name] !== undefined) { strStart = "<span style='font-size: "+ oEnlarge[name][0] +"px'>" strEnd = sc } let spacer = aSpacer.includes(name) ? " " : "" aDisplay.push("<div class='chars'>"+ strStart + spacer + aCharsMin.join(ZWNJ + spacer) + strEnd +"</div>") aDisplayMax.push("<div class='chars'>"+ strStart + spacer + aCharsMax.join(ZWNJ + spacer) + strEnd +"</div>") // reset grouptitle grouptitle = "" } } }) //console.log("'"+ namesDebug.join("',\n'") +"',") aDisplay.push("<a name='end'></a>") aDisplayMax.push("<a name='end'></a>") // counts let min20 =0, min50=0, minAll=0, max20=0, max50=0, maxAll=0 for (let i=0; i < aCountsMin.length; i++) { let minValue = aCountsMin[i], maxValue = aCountsMax[i] min20 += (minValue > 19 ? 20 : minValue) min50 += (minValue > 49 ? 50 : minValue) minAll += minValue max20 += (maxValue > 19 ? 20 : maxValue) max50 += (maxValue > 49 ? 50 : maxValue) maxAll += maxValue } oCodesCounts["min20"] = min20 oCodesCounts["min50"] = min50 oCodesCounts["minALL"] = minAll oCodesCounts["max20"] = max20 oCodesCounts["max50"] = max50 oCodesCounts["maxALL"] = maxAll // perf dom.perf.innerHTML = "setup: "+ Math.round(performance.now() - t0) +" ms | " + get_count("test ") // sort oTempData => sorted objects const names = Object.keys(oTempData).sort() dom.countScript.innerHTML = "["+ names.length +"]" var everyCode = [] for (const k of names) { oCodePoints[k] = oTempData[k] if (oTempData[k]["range"] !== undefined) { oRanges[k] = oTempData[k]["range"] } if (oTempData[k]["partial"] !== undefined) { let rangeChk = oTempData[k]["partial"] if (false !== rangeChk) { // ignore false oRangesReduced[k] = oTempData[k]["partial"] } } if (oTempData[k]["max used"] !== undefined) { oCodesMax[k] = oTempData[k]["max used"] if (aBlocksSkip.length > 0) { everyCode = everyCode.concat(oCodesMax[k]) } } if (oTempData[k]["ignored"] !== undefined) { oCodesIgnore[k] = oTempData[k]["ignored"] } if (oTempData[k]["reserved"] !== undefined) { oCodesReserved[k] = oTempData[k]["reserved"] } if (oTempData[k]["wiki"] !== undefined) { oWiki[k] = oTempData[k]["wiki"] } if (oTempData[k]["reduce"] !== undefined) { oCodesReduce[k] = oTempData[k]["reduce"] if (oTempData[k]["min used"] !== undefined) { oCodesMin[k] = oTempData[k]["min used"] } } if (oTempData[k]["min version"] !== undefined) { oCodesMinVersion[k] = oTempData[k]["min version"] } } if (aBlocksSkip.length > 0) { console.log(everyCode.length, everyCode) //console.log("['"+ everyCode.join("','") +"']") } // return return resolve() }) } function reset(resetperf = true, resetdisplay = true) { // reset/display everything to make sure all code points have time to hopefully render if (resetperf) { dom.perf.innerHTML = get_count("test ") } if (resetdisplay) { dom.legend.innerHTML = aLegendClean.join("") dom.visual.innerHTML = dom.allassigned.checked ? aDisplayMax.join("<br>") : aDisplay.join("<br>") dom.summary.innerHTML = "" } if (!resetperf) {isRunning = false} } function run() { if (!isRunning) { // we need to retest tofu size in case user zoomed Promise.all([ get_tofu_size(), reset(), ]).then(function(results){ try { isRunning = true dom.perf.innerHTML = "running ..." dom.runningStatus = "running ... "+ get_count("testing ") dom.runningTofuSize.innerHTML = tofuSize + " <span class='btn12 btnc' onClick='logConsole(`tofusize`)'>[details]</span>" let testDepth = document.querySelector('input[name="depth"]:checked').value testParams = (testDepth == "ALL" ? "all" : "up to "+ testDepth) +" | "+ (dom.allassigned.checked ? "max" : "reduced") dom.runningParameters = testParams setTimeout(function() { test(testDepth) }, 1) } catch(e) { console.log(e) } }) } } function test(testDepth) { dom.perf.innerHTML = "&nbsp" // vars let aVisual = [], aLegend = [], aSummary = [], countTested = 0, countTofu = 0 let div = dom.ugDiv, span = dom.ugSpan, slot = dom.ugSlot let t0 = performance.now() slot.style.fontFamily = "none" if (testDepth == "ALL") {testDepth = 800} let aRed = [], aOrange = [], aYellow = [] // reset tofu fpTofuData = {} fpTofuHashes = [] // reset size data fpSizeDataClient = {} // reset size hashes fpSizeHashesClient = [] // reset unique sizes fpSizeClientUnique = [] let oTempTofu = {}, oTempSizeClient = {}, oTempSizeClientUnique = {} aBlocks.forEach(function(data) { let name = data[0].toLowerCase() let anchor = strip(name) if (data[1] == undefined) { let firstItem = aBlocks[0].join() aLegend.push((name.toLowerCase() == firstItem.toLowerCase() ? "": "<br>") +"<a class='blue' href='#"+ anchor +"group'>"+ name.toUpperCase() +"</a><br>") } else { let go = !isTest if (isTest && aTest.includes(name)) {go = true} if (go) { let codes = dom.allassigned.checked ? oCodePoints[name]["max used"] : oCodePoints[name]["min used"] if (oCodePoints[name]["min used"] == undefined) {codes = oCodePoints[name]["max used"]} let displayA = [] let aTofu = [] let aSizeClient = [] let anchorColor = "no_color", nameColor = s12, partial = "" let maxDepth = (testDepth > codes.length ? codes.length : testDepth) maxDepth = maxDepth * 1 // split into test and remainder let codesA = codes.slice(0, maxDepth) let codesB = codes.slice(maxDepth, codes.length) countTested += codesA.length // test let aSpanBlock = [], aNonSpanBlock = [] let spacer = aSpacer.includes(name) ? " " : "" for (let i=0; i < codesA.length; i++) { let character = String.fromCodePoint(codesA[i]) slot.textContent = character // client: w=span, h=div let cDiv = div.getClientRects() let cSpan = span.getClientRects() let wClient = cSpan[0].width, hClient = cDiv[0].height let whClient = wClient +" x "+ hClient aSizeClient.push(codesA[i] +": "+ whClient) if (whClient == tofuSize) { aTofu.push(codesA[i]) aSpanBlock.push(character) if (aNonSpanBlock.length) { displayA.push(spacer + aNonSpanBlock.join(ZWNJ + spacer) + ZWNJ) aNonSpanBlock = [] // reset } } else { if (aSpanBlock.length) { displayA.push(sb + spacer + aSpanBlock.join(ZWNJ + spacer) + sc + ZWNJ) aSpanBlock = [] // reset } aNonSpanBlock.push(character) } // global unique sizes if (oTempSizeClientUnique[wClient] == undefined) { oTempSizeClientUnique[wClient] = [hClient] } else { oTempSizeClientUnique[wClient].push(hClient) } // ToDo: sizes + count per script } // add final bit if (aSpanBlock.length) { displayA.push(sb + spacer + aSpanBlock.join(ZWNJ + spacer) + sc + ZWNJ) } if (aNonSpanBlock.length) { displayA.push(spacer + aNonSpanBlock.join(ZWNJ + spacer) + ZWNJ) } // track tofu if (aTofu.length) { countTofu += aTofu.length let tofuHash = mini(aTofu.join()) oTempTofu[name] = {"hash": tofuHash, "data": aTofu} } oTempSizeClient[name] = {"hash": mini(aSizeClient.join()), "data": aSizeClient} // remainder if (codesB.length > 0) { let aRemainderBlock = [] for (let i=0; i < codesB.length; i++) { let character = String.fromCodePoint(codesB[i]) aRemainderBlock.push(character) } displayA.push(spacer + aRemainderBlock.join(ZWNJ + spacer) + ZWNJ) } // colors let percent = (aTofu.length/maxDepth) * 100 let strA = displayA.join("") if (percent >= 80) { anchorColor = "sb" nameColor = sb if (percent !== 100) {partial = " ["+ aTofu.length +"/" + maxDepth +"]"} aRed.push("<a class='no_color' href='#"+ anchor +"'>"+ name +"</a>" + sb + partial + sc) } else if (percent >= 20) { nameColor = s2 partial = s2 +" ["+ aTofu.length +"/" + maxDepth +"]"+ sc strA = strA.replace(/bad/g, "s2") aOrange.push("<a class='no_color' href='#"+ anchor +"'>"+ name +"</a>"+ partial) } else if (percent > 0) { //anchorColor = "s4" // too noisy, just let the count be colored nameColor = s4 partial = s4 +" ["+ aTofu.length +"/" + maxDepth +"]"+ sc strA = strA.replace(/bad/g, "s4") aYellow.push("<a class='no_color' href='#"+ anchor +"'>"+ name +"</a>"+ partial) } // display aLegend.push("<li><a class='"+ anchorColor +"' href='#"+ anchor +"'>"+ name +"</a>" + partial +"</li>") // append per script tofu + size clickables let fpBtnTofu = "" if (aTofu.length) { fpBtnTofu = " &nbsp <span class='btn14 btnc mono' onClick='logConsole(`fptofuscript`,`"+ name +"`)'>[tofu]</span>" } let fpBtnSize = " &nbsp <span class='btn12 btnc mono' onClick='logConsole(`fpsizescript`,`"+ name +"`)'>[sizes]</span>" aVisual.push(oScriptHeader[name][0] + fpBtnTofu + fpBtnSize) let strStart = "", strEnd = "" if (oEnlarge[name] !== undefined) { strStart = "<span style='font-size: "+ oEnlarge[name][0] +"px'>" strEnd = sc } aVisual.push("<div class='chars'>" + strStart + strA + strEnd +"</div>") } } }) // reset slot so we don't end up with unwanted scrollbars slot.textContent = "" // sort tofu const namesTofu = Object.keys(oTempTofu).sort() for (const nameT of namesTofu) { fpTofuData[nameT] = oTempTofu[nameT] fpTofuHashes.push(nameT +": "+ oTempTofu[nameT]["hash"]) } // sort sizes const namesClient = Object.keys(oTempSizeClient).sort() for (const nameC of namesClient) { fpSizeDataClient[nameC] = oTempSizeClient[nameC] fpSizeHashesClient.push(nameC +": "+ oTempSizeClient[nameC]["hash"]) } // unique sizes const namesUniqueSizes = Object.keys(oTempSizeClientUnique).sort((a,b) => a-b) for (const nameU of namesUniqueSizes) { let aHeight = oTempSizeClientUnique[nameU] aHeight = aHeight.filter(function(item, position) {return aHeight.indexOf(item) === position}) aHeight.sort((a,b) => a-b) aHeight.forEach(function(h) { fpSizeClientUnique.push(nameU +" x " + h) }) } // FP lines let tofuSizeBtn = " <span class='btn12 btnc' onClick='logConsole(`tofusize`)'>[details]</span>" let tofuFP = mini(fpTofuHashes.join()) + " <span class='btn12 btnc' onClick='logConsole(`fptofu`)'>[summary]</span>" + " <span class='btn12 btnc' onClick='logConsole(`fpTofuData`)'>["+ countTofu +"]</span> " + ((countTofu/countTested) * 100).toFixed(2) + "%" if (countTofu == 0) { tofuFP = testParams == "all | max" ? sg + "OMG! CONGRATS: NO TOFU"+ sc : sg +"none"+ sc +": up your test parameters" } let sizeFPClient = mini(fpSizeHashesClient.join()) + " <span class='btn12 btnc' onClick='logConsole(`fpSizeClient`)'>[summary]</span>" + " <span class='btn12 btnc' onClick='logConsole(`fpSizeDataClient`)'>["+ countTested +"]</span> " + " <span class='btn12 btnc' onClick='logConsole(`fpSizeDataClientUnique`)'>["+ fpSizeClientUnique.length +" sizes]</span> " let strFP = "<span class='mono'>" + s12 +"tofu size: ".padStart(16) + sc + tofuSize + tofuSizeBtn + "<br>" + s12 +"parameters: ".padStart(16) + sc + testParams +"<br>" + s12 +"tofu: ".padStart(16) + sc + tofuFP +"<br>" + s12 +"getClientRects: ".padStart(16) + sc + sizeFPClient +"</span><br>" // display visuals aVisual.push("<a name='end'></a>") dom.visual.innerHTML = strFP +"<br>"+ aVisual.join("<br>") dom.legend.innerHTML = aLegend.join("") +"<br><hr><br>" // summary if (aRed.length) { aRed.sort() aSummary.push("<br>"+ sb + "unsupported [" + aRed.length +"]"+ sc +"<br>") aSummary.push("<br><li>"+ aRed.join("</li><li>") +"</li>") } if (aOrange.length) { aOrange.sort() aSummary.push("<br>"+ s2 + "mixed [" + aOrange.length +"]"+ sc +"<br>") aSummary.push("<br><li>" + aOrange.join("</li><li>") +"</li>") } if (aYellow.length) { aYellow.sort() aSummary.push("<br>"+ s4 + "partial [" + aYellow.length +"]"+ sc +"<br>") aSummary.push("<br><li>"+ aYellow.join("</li><li>") +"</li>") } dom.summary.innerHTML = "<span class='s12' style='font-size: 12px;'><b><u>" + "SUMMARY</u></b>"+ sc +"<br>"+ aSummary.join("") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms | " + get_count("") isRunning = false } //dom.allassigned.checked = true let isRunning = true setTimeout(function() { Promise.all([ get_tofu_size(), generate(), ]).then(function(){ reset(false) // false: leave the generate perf value there }) }, 1) </script> </body> </html> ================================================ FILE: tests/fontsmac.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>mac fonts</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 780px;} indent {margin-left: 20px;} </style> </head> <body> <div class="offscreen"> <div class="normalized"><span id="dfsize"></span></div> <div><span id="dfproportion"></span></div> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <thead><tr><th> <div class="nav-title">mac fonts</div> </th></tr></thead> <tr><td class="intro"><span class="no_color"> Programmatically make sense of Apple's macOS font lists. The <code>group</code> option is to aid visual parsing, and does <b>not</b> signify <code>font-families</code>.</br><br> <span class="btn12 btnfirst" onclick="get_links()">links</span> | <span class="btn12 btn" onclick="get_list()">list</span> &nbsp; <select id="optList" name="optList" onchange="get_list()"></select> &nbsp; <input id="optGroup" type="checkbox" onclick="get_list()" checked> group &nbsp; | <span class="btn12 btn" onclick="analyse()">analyse since</span> &nbsp;<select id="optAnalyze" name="optAnalyse" onchange="analyse()"></select> &nbsp; | <span class="btn12 btn" id="txtSearch" onclick="search()">search</span> &nbsp;<input type="text" id="optSearch" style="width:100px;"></input> &nbsp; <span id="alert" class='bad mono'></span> </span></td></tr> <tr><td><hr><br></td></tr> <tr> <td class="mono" style="text-align: left"> <span class="spaces no_color" id="results"></span> </td> </tr> </table> <br> <script> 'use strict'; let fntAll = {list: [], group: []} let aLinks = [] let currentDisplay = '' function analyse() { let key = dom.optAnalyse.value let newDisplay = key +'analyse' if (currentDisplay == newDisplay) {return} let aDisplay = [] let isBase = false, start='', end='' let fntsEncountered = [] let fntDiffs = {} let aCommon = [], aBase = [] for (const k of Object.keys(fntData).sort()) { if (k == key) { // base starting data isBase = true start = fntData[k].version +' '+ fntData[k].name fntsEncountered = fntData[k].list aCommon = fntData[k].list aBase = fntData[k].list } else { // analyse items after base let aNew = fntData[k].list if (isBase && undefined !== aNew && aNew.length) { end = fntData[k].version +' '+ fntData[k].name fntsEncountered = fntsEncountered.concat(fntData[k].list) fntDiffs[k] = {} // common aCommon = aCommon.filter(x => aNew.includes(x)) // use previous aBase to compare fntDiffs[k].removed = aBase.filter(x => !aNew.includes(x)) fntDiffs[k].added = aNew.filter(x => !aBase.includes(x)) // set aBase for next k aBase = fntData[k].list } } } fntsEncountered = fntsEncountered.filter(function(item, position) {return fntsEncountered.indexOf(item) === position}) aDisplay.push(s14 +'ANALYSIS: '+ sc + s12 + start + sc + ' to '+ s12 + end + sc +' ['+ fntsEncountered.length +']<br>') // TB let aTB = fntTB.list let aNotInTB = aTB.filter(x => !aCommon.includes(x)) let strTB = aNotInTB.length ? '' : sg +' ['+ green_tick +' all fonts in common]'+ sc // common let objCommon = group(aCommon) let aCommonDisplay = group_display(objCommon) aDisplay.push('<details><summary>'+ s12 +'IN COMMON ' + sc + '['+ aCommon.length +']</summary><p>' + aCommonDisplay.join('<br>') +'</p></details>') aDisplay.push('<details><summary>'+ s12 +'TB SYSTEM FONTS ' + sc +'['+ fntTB.list.length + strTB +']</summary><p>' + fntTB.group.join('<br>') +'</p></details>') // TB missing if (aNotInTB.length) { let objNotTB = group(aNotInTB) let aNotTB = group_display(objNotTB) aDisplay.push(sb +'TB SYSTEM FONTS not IN COMMON '+ sc + '['+ aNotInTB.length +']<br><br>' + aNotTB.join('<br>') +'<br>' ) } // DIFFS aDisplay.push('<hr></br>' + s14 +'DIFFS'+ sc +'<br>') //console.log(fntDiffs) for (const k of Object.keys(fntDiffs)) { // assume some changes aDisplay.push(s12 + fntData[k].version +' '+ fntData[k].name + sc +'<br>') for (const j of Object.keys(fntDiffs[k]).sort()) { let list = fntDiffs[k][j] if (list.length) { let objSub = group(list) let aSub = group_display(objSub) if (list.length < 40) { aDisplay.push(j +' ['+ list.length +']<br><span class="indent">' + aSub.join('<br>') +'</span><br>' ) } else { aDisplay.push('<details><summary>'+ j +' ['+ list.length +']</summary><p>' + aSub.join('<br>') +'</p></details>') } } } } // ALL let objEncountered = group(fntsEncountered) let aEncountered = group_display(objEncountered) aDisplay.push('<hr></br>') aDisplay.push('<details><summary>'+ s14 + 'ALL FONTS'+ sc +' since ' + s14 + start +sc +' ['+ fntsEncountered.length +']</summary><p>' + aEncountered.join('<br>') +'</p></details>') currentDisplay = newDisplay dom.results.innerHTML = aDisplay.join('<br>') //console.log('displaying:', newDisplay) } function get_links() { dom.results.innerHTML = aLinks.join('<br>') currentDisplay = 'links' } function get_list() { let k = dom.optList.value let newDisplay = k +'list'+ dom.optGroup.checked if (currentDisplay == newDisplay) {return} let aDisplay = [], obj if ('all' == k) { let i = 0, start = '', end = '' for (const k of Object.keys(fntData).sort()) { i++ if (1 == i) {start = fntData[k].version +' '+ fntData[k].name } else { end = fntData[k].version +' '+ fntData[k].name } } aDisplay.push(s12 + start + sc + ' to '+ s12 + end + sc +' ['+ fntAll.list.length +']<br>') obj = fntAll } else { let link = '<a class="blue" target="_blank" href="'+ fntData[k].url+ '">support link</a>' aDisplay.push(s12 + fntData[k].version +' '+ fntData[k].name + sc +' ['+ fntData[k].count +'] '+ link +'<br>') obj = fntData[k] } let data = dom.optGroup.checked ? obj.group : obj.list if (undefined !== data) { aDisplay.push(data.join('<br>')) dom.results.innerHTML = aDisplay.join('<br>') currentDisplay = newDisplay //console.log('displaying:', newDisplay) } } function group(array) { // return obj let obj = {} array.forEach(function(font){ let key = font.split(' ')[0] let key5 = key.slice(0,5) if ('Noto' == key) { key += ' '+ font.split(' ')[1] } else if ('Nanum' == key5 || 'Akaya' == key5 || 'Apple' == key5 || 'Chalk' == key5) { key = key5 } else if ('STIX' == key.slice(0,4)) { key = key.slice(0,4) } else if ('BiauKai' == key.slice(0,7)) { key = key.slice(0,7) } else { let key2 = font.split('-')[0] if (key2.length < key.length) {key = key2} } if (undefined == obj[key]) {obj[key] = [font]} else {obj[key].push(font)} }) return obj } function group_display(obj, k) { let array = [] for (const j of (Object.keys(obj).sort())) { array.push(s6 + j + sc +' {<br><span class="faint indent">'+ obj[j].join(', ') +'</span><br>}') // if k provided then we also build fntList data if (undefined !== k) {fntData[k].data[j] = obj[j]} } return array } function run_once() { // build lists for analysis let aOptionsList = [] let allSet = new Set() // all unique font names: so we can help track we don't "dupe" across releases e.g. case, spaces let allLower = new Set() fntAll = {list: [], group: []} aLinks = [s12 +'SUPPORT LINKS'+ sc +'<br>'] let aAll = [] for (const k of Object.keys(fntData).sort()) { if (fntData[k].list.length) { aOptionsList.push('<option value="'+ k +'">' + fntData[k].name +' ('+ fntData[k].version +')</option>') let array = fntData[k].list.sort() aAll = aAll.concat(array) fntData[k].count = array.length // unique lower case array.forEach(function(font){allLower.add(font.toLowerCase())}) // group let obj = group(array) fntData[k].data = {} fntData[k].group = group_display(obj, k) /* tidy up into lines for fntData[k].list let aPlain = [] for (const g of Object.keys(obj).sort()) {aPlain.push("'" + obj[g].join("','") +"'")} console.log(k +'\n', aPlain.join(",\n") + ",") //*/ // links let item = fntData[k].version +' '+ fntData[k].name item = item.padStart(15) +': ' item += '<a class="blue" href="'+ fntData[k].url +'" target="_blank">' + fntData[k].url +'</a>' aLinks.push(item + '<br>') } } // dedupe all unique names into a sorted array aAll = aAll.filter(function(item, position) {return aAll.indexOf(item) === position}) aAll.sort() fntAll['list'] = aAll let aAllLower = Array.from(allLower) aAllLower.sort() if (aAll.length !== aAllLower.length) { console.error('all length ', aAll.length, '!== all lowercase length', aAllLower.length) console.log(aAll.join('\n')) dom.alert.innerHTML = '[dupes detected]' } // group all let objall = group(aAll) fntAll.group = group_display(objall) // populate options aOptionsList.push('<option value="all">ALL</option>') dom.optList.innerHTML = aOptionsList.join('') let aOptionsAnalyse = aOptionsList.slice(0, aOptionsList.length - 2) dom.optAnalyse.innerHTML = aOptionsAnalyse.join('') // TB fonts fntTB.list.sort() let objTB = group(fntTB.list) fntTB.group = group_display(objTB) // add search listener let target = dom.optSearch target.addEventListener("keypress", function(event) { if (event.key === "Enter") {search()} }) // display something analyse() } function search() { let term = (dom.optSearch.value).trim() if (0 == term.length) {return} currentDisplay = 'search' try { let search = term.toLowerCase() // case-insenstive let oData = {}, isFound = false for (const k of Object.keys(fntData).sort()) { if (fntData[k].list.length) { let aFound = [] let aSearch = fntData[k].list.sort() aSearch.forEach(function(item){ let value = item.toLowerCase() if (value.includes(search)) {aFound.push(item)} }) if (aFound.length) { isFound = true oData[fntData[k].version +' '+ fntData[k].name] = aFound } } } let aDisplay = [s14 +'SEARCH'+ sc +': ' + term +'<br>'] if (isFound) { //console.log(oData) for (const k of Object.keys(oData).sort()) { aDisplay.push(s12 + k + sc) aDisplay.push('<br><div class="indent">'+ oData[k].join(', ') +'</div><br>') } } else { aDisplay.push('no results') } dom.results.innerHTML = aDisplay.join('<br>') } catch(e) { dom.results.innerHTML = e+'' } } let fntTB = { // system fonts list: [ // allowlist 'AppleGothic','Apple Color Emoji','Arial','Arial Black','Arial Narrow','Courier','Courier New', 'Geneva','Georgia', //'Heiti TC', // this is not a font per se, it comes in light + medium 'Helvetica','Helvetica Neue','Hiragino Kaku Gothic ProN', 'Kailasa','Lucida Grande','Menlo','Monaco','PingFang HK','PingFang SC','PingFang TC','Songti SC', 'Songti TC','Tahoma','Thonburi','Times','Times New Roman','Verdana', // weighted/styles 'Arial Bold','Arial Bold Italic','Arial Italic', 'Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic', 'Courier Bold','Courier Bold Oblique','Courier Oblique', 'Courier New Bold','Courier New Bold Italic','Courier New Italic', 'Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Heiti TC Light','Heiti TC Medium', 'Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Oblique', 'Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Italic','Helvetica Neue Light', 'Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin', 'Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic', 'Hiragino Kaku Gothic ProN W3','Hiragino Kaku Gothic ProN W6', 'Kailasa Bold', 'Lucida Grande Bold', 'Menlo Bold','Menlo Bold Italic','Menlo Italic', 'PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight', 'PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight', 'PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Songti SC Black','Songti SC Bold','Songti SC Light', 'Songti TC Bold','Songti TC Light', 'Tahoma Bold', 'Thonburi Bold','Thonburi Light', 'Times Bold','Times Bold Italic','Times Italic', 'Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Verdana Bold','Verdana Bold Italic','Verdana Italic', ] } /* NOTE: except 10.15 + 11, lists don't distinguish between built-in vs downloadable catalina 10.15: https://support.apple.com/en-us/101429 big sur 11: https://support.apple.com/en-us/101430 monterey 12: https://support.apple.com/en-us/103203 ventura 13: https://support.apple.com/en-nz/103197 sonoma 14: https://support.apple.com/en-nz/108939 sequoia 15: https://support.apple.com/en-us/120414 tahoe 26: https://support.apple.com/en-us/122869 group by first part of font name: i.e don't distinguish between Avenir vs Avenir Book: to break it up somewhat for easier analysis/parsing */ let fntData = { 'v10.15': { name: 'Catalina', version: 10.15, url: 'https://support.apple.com/en-us/101429', list: [ 'Al Bayan Bold','Al Bayan','Al Nile','Al Nile Bold','Al Tarikh', 'American Typewriter','American Typewriter Bold','American Typewriter Condensed','American Typewriter Condensed Bold','American Typewriter Condensed Light','American Typewriter Light','American Typewriter Semibold', 'Andale Mono', 'Apple Braille','Apple Braille Outline 6 Dot','Apple Braille Outline 8 Dot','Apple Braille Pinpoint 6 Dot','Apple Braille Pinpoint 8 Dot','Apple Chancery','Apple SD Gothic Neo','Apple SD Gothic Neo Bold','Apple SD Gothic Neo ExtraBold','Apple SD Gothic Neo Heavy','Apple SD Gothic Neo Light','Apple SD Gothic Neo Medium','Apple SD Gothic Neo SemiBold','Apple SD Gothic Neo Thin','Apple SD Gothic Neo UltraLight','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Black','Arial Bold','Arial Bold Italic','Arial Hebrew','Arial Hebrew Bold','Arial Hebrew Light','Arial Hebrew Scholar','Arial Hebrew Scholar Bold','Arial Hebrew Scholar Light','Arial Italic','Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic','Arial Rounded MT Bold','Arial Unicode MS', 'Avenir Black','Avenir Black Oblique','Avenir Book','Avenir Book Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light','Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Next','Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Condensed','Avenir Next Condensed Bold','Avenir Next Condensed Bold Italic','Avenir Next Condensed Demi Bold','Avenir Next Condensed Demi Bold Italic','Avenir Next Condensed Heavy','Avenir Next Condensed Heavy Italic','Avenir Next Condensed Italic','Avenir Next Condensed Medium','Avenir Next Condensed Medium Italic','Avenir Next Condensed Ultra Light','Avenir Next Condensed Ultra Light Italic','Avenir Next Demi Bold','Avenir Next Demi Bold Italic','Avenir Next Heavy','Avenir Next Heavy Italic','Avenir Next Italic','Avenir Next Medium','Avenir Next Medium Italic','Avenir Next Ultra Light','Avenir Next Ultra Light Italic','Avenir Oblique','Avenir Roman', 'Ayuthaya', 'Baghdad', 'Bangla MN','Bangla MN Bold','Bangla Sangam MN','Bangla Sangam MN Bold', 'Baskerville','Baskerville Bold','Baskerville Bold Italic','Baskerville Italic','Baskerville SemiBold','Baskerville SemiBold Italic', 'Beirut', 'Big Caslon Medium', 'Bodoni 72 Bold','Bodoni 72 Book','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book','Bodoni 72 Oldstyle Book Italic','Bodoni 72 Smallcaps Book','Bodoni Ornaments', 'Bradley Hand Bold', 'Brush Script MT Italic', 'Chalkboard','Chalkboard Bold','Chalkboard SE','Chalkboard SE Bold','Chalkboard SE Light','Chalkduster', 'Charter Black','Charter Black Italic','Charter Bold','Charter Bold Italic','Charter Italic','Charter Roman', 'Cochin','Cochin Bold','Cochin Bold Italic','Cochin Italic', 'Comic Sans MS','Comic Sans MS Bold', 'Copperplate','Copperplate Bold','Copperplate Light', 'Corsiva Hebrew','Corsiva Hebrew Bold', 'Courier','Courier Bold','Courier Bold Oblique','Courier New','Courier New Bold','Courier New Bold Italic','Courier New Italic','Courier Oblique', 'DIN Alternate Bold','DIN Condensed Bold', 'Damascus','Damascus Bold','Damascus Light','Damascus Medium','Damascus Semi Bold', 'DecoType Naskh', 'Devanagari MT','Devanagari MT Bold','Devanagari Sangam MN','Devanagari Sangam MN Bold', 'Didot','Didot Bold','Didot Italic', 'Diwan Kufi','Diwan Thuluth', 'Euphemia UCAS','Euphemia UCAS Bold','Euphemia UCAS Italic', 'Farah', 'Farisi', 'Futura Bold','Futura Condensed ExtraBold','Futura Condensed Medium','Futura Medium','Futura Medium Italic', 'GB18030 Bitmap', 'Galvji','Galvji Bold','Galvji Bold Oblique','Galvji Oblique', 'Geeza Pro','Geeza Pro Bold', 'Geneva', 'Georgia','Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Gill Sans','Gill Sans Bold','Gill Sans Bold Italic','Gill Sans Italic','Gill Sans Light','Gill Sans Light Italic','Gill Sans SemiBold','Gill Sans SemiBold Italic','Gill Sans UltraBold', 'Gujarati MT','Gujarati MT Bold','Gujarati Sangam MN','Gujarati Sangam MN Bold', 'Gurmukhi MN','Gurmukhi MN Bold','Gurmukhi MT','Gurmukhi Sangam MN','Gurmukhi Sangam MN Bold', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Helvetica','Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Neue','Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Condensed Black','Helvetica Neue Condensed Bold','Helvetica Neue Italic','Helvetica Neue Light','Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin','Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic','Helvetica Oblique', 'Herculanum', 'Hiragino Maru Gothic ProN W4','Hiragino Mincho ProN W3','Hiragino Mincho ProN W6','Hiragino Sans GB W3','Hiragino Sans GB W6','Hiragino Sans W0','Hiragino Sans W1','Hiragino Sans W2','Hiragino Sans W3','Hiragino Sans W4','Hiragino Sans W5','Hiragino Sans W6','Hiragino Sans W7','Hiragino Sans W8','Hiragino Sans W9', 'Hoefler Text','Hoefler Text Black','Hoefler Text Black Italic','Hoefler Text Italic','Hoefler Text Ornaments', 'ITF Devanagari Bold','ITF Devanagari Book','ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Marathi Bold','ITF Devanagari Marathi Book','ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium','ITF Devanagari Medium', 'Impact', 'InaiMathi','InaiMathi Bold', 'Kailasa','Kailasa Bold', 'Kannada MN','Kannada MN Bold','Kannada Sangam MN','Kannada Sangam MN Bold', 'Kefa','Kefa Bold', 'Khmer MN','Khmer MN Bold','Khmer Sangam MN', 'Kohinoor Bangla','Kohinoor Bangla Bold','Kohinoor Bangla Light','Kohinoor Bangla Medium','Kohinoor Bangla Semibold','Kohinoor Devanagari','Kohinoor Devanagari Bold','Kohinoor Devanagari Light','Kohinoor Devanagari Medium','Kohinoor Devanagari Semibold','Kohinoor Gujarati','Kohinoor Gujarati Bold','Kohinoor Gujarati Light','Kohinoor Gujarati Medium','Kohinoor Gujarati Semibold','Kohinoor Telugu','Kohinoor Telugu Bold','Kohinoor Telugu Light','Kohinoor Telugu Medium','Kohinoor Telugu Semibold', 'Kokonor', 'Krungthep', 'KufiStandardGK', 'Lao MN','Lao MN Bold','Lao Sangam MN', 'Lucida Grande','Lucida Grande Bold', 'Luminari', 'Malayalam MN','Malayalam MN Bold','Malayalam Sangam MN','Malayalam Sangam MN Bold', 'Marker Felt Thin','Marker Felt Wide', 'Menlo','Menlo Bold','Menlo Bold Italic','Menlo Italic', 'Microsoft Sans Serif', 'Mishafi','Mishafi Gold', 'Monaco', 'Mshtakan','Mshtakan Bold','Mshtakan BoldOblique','Mshtakan Oblique', 'Mukta Mahee','Mukta Mahee Bold','Mukta Mahee ExtraBold','Mukta Mahee ExtraLight','Mukta Mahee Light','Mukta Mahee Medium','Mukta Mahee SemiBold', 'Muna','Muna Black','Muna Bold', 'Myanmar MN','Myanmar MN Bold','Myanmar Sangam MN','Myanmar Sangam MN Bold', 'Nadeem', 'New Peninim MT','New Peninim MT Bold','New Peninim MT Bold Inclined','New Peninim MT Inclined', 'Noteworthy Bold','Noteworthy Light', 'Noto Nastaliq Urdu','Noto Nastaliq Urdu Bold', 'Noto Sans Javanese','Noto Sans Kannada','Noto Sans Kannada Black','Noto Sans Kannada Bold','Noto Sans Kannada ExtraBold','Noto Sans Kannada ExtraLight','Noto Sans Kannada Light','Noto Sans Kannada Medium','Noto Sans Kannada SemiBold','Noto Sans Kannada Thin','Noto Sans Myanmar','Noto Sans Myanmar Black','Noto Sans Myanmar Bold','Noto Sans Myanmar ExtraBold','Noto Sans Myanmar ExtraLight','Noto Sans Myanmar Light','Noto Sans Myanmar Medium','Noto Sans Myanmar SemiBold','Noto Sans Myanmar Thin','Noto Sans Oriya','Noto Sans Oriya Bold', 'Noto Serif Myanmar','Noto Serif Myanmar Black','Noto Serif Myanmar Bold','Noto Serif Myanmar ExtraBold','Noto Serif Myanmar ExtraLight','Noto Serif Myanmar Light','Noto Serif Myanmar Medium','Noto Serif Myanmar SemiBold','Noto Serif Myanmar Thin', 'Optima','Optima Bold','Optima Bold Italic','Optima ExtraBlack','Optima Italic', 'Oriya MN','Oriya MN Bold','Oriya Sangam MN','Oriya Sangam MN Bold', 'PT Mono','PT Mono Bold','PT Sans','PT Sans Bold','PT Sans Bold Italic','PT Sans Caption','PT Sans Caption Bold','PT Sans Italic','PT Sans Narrow','PT Sans Narrow Bold','PT Serif','PT Serif Bold','PT Serif Bold Italic','PT Serif Caption','PT Serif Caption Italic','PT Serif Italic', 'Palatino','Palatino Bold','Palatino Bold Italic','Palatino Italic', 'Papyrus','Papyrus Condensed', 'Phosphate Inline','Phosphate Solid', 'PingFang HK','PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight','PingFang SC','PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight','PingFang TC','PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Plantagenet Cherokee', 'Raanana','Raanana Bold', 'Rockwell','Rockwell Bold','Rockwell Bold Italic','Rockwell Italic', 'STIXGeneral','STIXGeneral-Bold','STIXGeneral-BoldItalic','STIXGeneral-Italic','STIXIntegralsD','STIXIntegralsD-Bold','STIXIntegralsSm','STIXIntegralsSm-Bold','STIXIntegralsUp','STIXIntegralsUp-Bold','STIXIntegralsUpD','STIXIntegralsUpD-Bold','STIXIntegralsUpSm','STIXIntegralsUpSm-Bold','STIXNonUnicode','STIXNonUnicode-Bold','STIXNonUnicode-BoldItalic','STIXNonUnicode-Italic','STIXSizeFiveSym','STIXSizeFourSym','STIXSizeFourSym-Bold','STIXSizeOneSym','STIXSizeOneSym-Bold','STIXSizeThreeSym','STIXSizeThreeSym-Bold','STIXSizeTwoSym','STIXSizeTwoSym-Bold','STIXVariants','STIXVariants-Bold', 'STSong', 'Sana', 'Sathu', 'Savoye LET', 'Shree Devanagari 714','Shree Devanagari 714 Bold','Shree Devanagari 714 Bold Italic','Shree Devanagari 714 Italic', 'SignPainter','SignPainter Semibold', 'Silom', 'Sinhala MN','Sinhala MN Bold','Sinhala Sangam MN','Sinhala Sangam MN Bold', 'Skia','Skia Black','Skia Black Condensed','Skia Black Extended','Skia Bold','Skia Condensed','Skia Extended','Skia Light','Skia Light Condensed','Skia Light Extended', 'Snell Roundhand','Snell Roundhand Black','Snell Roundhand Bold', 'Songti SC','Songti SC Black','Songti SC Bold','Songti SC Light','Songti TC','Songti TC Bold','Songti TC Light', 'Sukhumvit Set Bold','Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Semi Bold','Sukhumvit Set Text','Sukhumvit Set Thin', 'Symbol', 'Tahoma','Tahoma Bold', 'Tamil MN','Tamil MN Bold','Tamil Sangam MN','Tamil Sangam MN Bold', 'Telugu MN','Telugu MN Bold','Telugu Sangam MN','Telugu Sangam MN Bold', 'Thonburi','Thonburi Bold','Thonburi Light', 'Times Bold','Times Bold Italic','Times Italic','Times New Roman','Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic','Times Roman', 'Trattatello', 'Trebuchet MS','Trebuchet MS Bold','Trebuchet MS Bold Italic','Trebuchet MS Italic', 'Verdana','Verdana Bold','Verdana Bold Italic','Verdana Italic', 'Waseem','Waseem Light', 'Webdings', 'Wingdings','Wingdings 2','Wingdings 3', 'Zapf Dingbats', 'Zapfino', ] }, 'v11': { name: 'Big Sur', version: 11, url: 'https://support.apple.com/en-us/101430', list: [ 'Al Bayan Bold','Al Bayan','Al Nile','Al Nile Bold','Al Tarikh', 'American Typewriter','American Typewriter Bold','American Typewriter Condensed','American Typewriter Condensed Bold','American Typewriter Condensed Light','American Typewriter Light','American Typewriter Semibold', 'Andale Mono', 'Apple Braille','Apple Braille Outline 6 Dot','Apple Braille Outline 8 Dot','Apple Braille Pinpoint 6 Dot','Apple Braille Pinpoint 8 Dot','Apple Chancery','Apple Color Emoji','Apple SD Gothic Neo','Apple SD Gothic Neo Bold','Apple SD Gothic Neo ExtraBold','Apple SD Gothic Neo Heavy','Apple SD Gothic Neo Light','Apple SD Gothic Neo Medium','Apple SD Gothic Neo SemiBold','Apple SD Gothic Neo Thin','Apple SD Gothic Neo UltraLight','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Black','Arial Bold','Arial Bold Italic','Arial Hebrew','Arial Hebrew Bold','Arial Hebrew Light','Arial Hebrew Scholar','Arial Hebrew Scholar Bold','Arial Hebrew Scholar Light','Arial Italic','Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic','Arial Rounded MT Bold','Arial Unicode MS', 'Avenir Black','Avenir Black Oblique','Avenir Book','Avenir Book Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light','Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Next','Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Condensed','Avenir Next Condensed Bold','Avenir Next Condensed Bold Italic','Avenir Next Condensed Demi Bold','Avenir Next Condensed Demi Bold Italic','Avenir Next Condensed Heavy','Avenir Next Condensed Heavy Italic','Avenir Next Condensed Italic','Avenir Next Condensed Medium','Avenir Next Condensed Medium Italic','Avenir Next Condensed Ultra Light','Avenir Next Condensed Ultra Light Italic','Avenir Next Demi Bold','Avenir Next Demi Bold Italic','Avenir Next Heavy','Avenir Next Heavy Italic','Avenir Next Italic','Avenir Next Medium','Avenir Next Medium Italic','Avenir Next Ultra Light','Avenir Next Ultra Light Italic','Avenir Oblique','Avenir Roman', 'Ayuthaya', 'Baghdad', 'Bangla MN','Bangla MN Bold','Bangla Sangam MN','Bangla Sangam MN Bold', 'Baskerville','Baskerville Bold','Baskerville Bold Italic','Baskerville Italic','Baskerville SemiBold','Baskerville SemiBold Italic', 'Beirut', 'Big Caslon Medium', 'Bodoni 72 Bold','Bodoni 72 Book','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book','Bodoni 72 Oldstyle Book Italic','Bodoni 72 Smallcaps Book','Bodoni Ornaments', 'Bradley Hand Bold', 'Brush Script MT Italic', 'Chalkboard','Chalkboard Bold','Chalkboard SE','Chalkboard SE Bold','Chalkboard SE Light','Chalkduster', 'Charter Black','Charter Black Italic','Charter Bold','Charter Bold Italic','Charter Italic','Charter Roman', 'Cochin','Cochin Bold','Cochin Bold Italic','Cochin Italic', 'Comic Sans MS','Comic Sans MS Bold', 'Copperplate','Copperplate Bold','Copperplate Light', 'Corsiva Hebrew','Corsiva Hebrew Bold', 'Courier','Courier Bold','Courier Bold Oblique','Courier New','Courier New Bold','Courier New Bold Italic','Courier New Italic','Courier Oblique', 'DIN Alternate Bold','DIN Condensed Bold', 'Damascus','Damascus Bold','Damascus Light','Damascus Medium','Damascus Semi Bold', 'DecoType Naskh', 'Devanagari MT','Devanagari MT Bold','Devanagari Sangam MN','Devanagari Sangam MN Bold', 'Didot','Didot Bold','Didot Italic', 'Diwan Kufi','Diwan Thuluth', 'Euphemia UCAS','Euphemia UCAS Bold','Euphemia UCAS Italic', 'Farah', 'Farisi', 'Futura Bold','Futura Condensed ExtraBold','Futura Condensed Medium','Futura Medium','Futura Medium Italic', 'GB18030 Bitmap', 'Galvji','Galvji Bold','Galvji Bold Oblique','Galvji Oblique', 'Geeza Pro','Geeza Pro Bold', 'Geneva', 'Georgia','Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Gill Sans','Gill Sans Bold','Gill Sans Bold Italic','Gill Sans Italic','Gill Sans Light','Gill Sans Light Italic','Gill Sans SemiBold','Gill Sans SemiBold Italic','Gill Sans UltraBold', 'Gujarati MT','Gujarati MT Bold','Gujarati Sangam MN','Gujarati Sangam MN Bold', 'Gurmukhi MN','Gurmukhi MN Bold','Gurmukhi MT','Gurmukhi Sangam MN','Gurmukhi Sangam MN Bold', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Helvetica','Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Neue','Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Condensed Black','Helvetica Neue Condensed Bold','Helvetica Neue Italic','Helvetica Neue Light','Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin','Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic','Helvetica Oblique', 'Herculanum', 'Hiragino Maru Gothic ProN W4','Hiragino Mincho ProN W3','Hiragino Mincho ProN W6','Hiragino Sans GB W3','Hiragino Sans GB W6','Hiragino Sans W0','Hiragino Sans W1','Hiragino Sans W2','Hiragino Sans W3','Hiragino Sans W4','Hiragino Sans W5','Hiragino Sans W6','Hiragino Sans W7','Hiragino Sans W8','Hiragino Sans W9', 'Hoefler Text','Hoefler Text Black','Hoefler Text Black Italic','Hoefler Text Italic','Hoefler Text Ornaments', 'ITF Devanagari Bold','ITF Devanagari Book','ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Marathi Bold','ITF Devanagari Marathi Book','ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium','ITF Devanagari Medium', 'Impact', 'InaiMathi','InaiMathi Bold', 'Kailasa','Kailasa Bold', 'Kannada MN','Kannada MN Bold','Kannada Sangam MN','Kannada Sangam MN Bold', 'Kefa','Kefa Bold', 'Khmer MN','Khmer MN Bold','Khmer Sangam MN', 'Kohinoor Bangla','Kohinoor Bangla Bold','Kohinoor Bangla Light','Kohinoor Bangla Medium','Kohinoor Bangla Semibold','Kohinoor Devanagari','Kohinoor Devanagari Bold','Kohinoor Devanagari Light','Kohinoor Devanagari Medium','Kohinoor Devanagari Semibold','Kohinoor Gujarati','Kohinoor Gujarati Bold','Kohinoor Gujarati Light','Kohinoor Gujarati Medium','Kohinoor Gujarati Semibold','Kohinoor Telugu','Kohinoor Telugu Bold','Kohinoor Telugu Light','Kohinoor Telugu Medium','Kohinoor Telugu Semibold', 'Kokonor', 'Krungthep', 'KufiStandardGK', 'Lao MN','Lao MN Bold','Lao Sangam MN', 'Lucida Grande','Lucida Grande Bold', 'Luminari', 'Malayalam MN','Malayalam MN Bold','Malayalam Sangam MN','Malayalam Sangam MN Bold', 'Marker Felt Thin','Marker Felt Wide', 'Menlo','Menlo Bold','Menlo Bold Italic','Menlo Italic', 'Microsoft Sans Serif', 'Mishafi','Mishafi Gold', 'Monaco', 'Mshtakan','Mshtakan Bold','Mshtakan BoldOblique','Mshtakan Oblique', 'Mukta Mahee','Mukta Mahee Bold','Mukta Mahee ExtraBold','Mukta Mahee ExtraLight','Mukta Mahee Light','Mukta Mahee Medium','Mukta Mahee SemiBold', 'Muna','Muna Black','Muna Bold', 'Myanmar MN','Myanmar MN Bold','Myanmar Sangam MN','Myanmar Sangam MN Bold', 'Nadeem', 'New Peninim MT','New Peninim MT Bold','New Peninim MT Bold Inclined','New Peninim MT Inclined', 'Noteworthy Bold','Noteworthy Light', 'Noto Nastaliq Urdu','Noto Nastaliq Urdu Bold', 'Noto Sans Kannada','Noto Sans Kannada Black','Noto Sans Kannada Bold','Noto Sans Kannada ExtraBold','Noto Sans Kannada ExtraLight','Noto Sans Kannada Light','Noto Sans Kannada Medium','Noto Sans Kannada SemiBold','Noto Sans Kannada Thin','Noto Sans Myanmar','Noto Sans Myanmar Black','Noto Sans Myanmar Bold','Noto Sans Myanmar ExtraBold','Noto Sans Myanmar ExtraLight','Noto Sans Myanmar Light','Noto Sans Myanmar Medium','Noto Sans Myanmar SemiBold','Noto Sans Myanmar Thin','Noto Sans Oriya','Noto Sans Oriya Bold', 'Noto Serif Myanmar','Noto Serif Myanmar Black','Noto Serif Myanmar Bold','Noto Serif Myanmar ExtraBold','Noto Serif Myanmar ExtraLight','Noto Serif Myanmar Light','Noto Serif Myanmar Medium','Noto Serif Myanmar SemiBold','Noto Serif Myanmar Thin', 'Optima','Optima Bold','Optima Bold Italic','Optima ExtraBlack','Optima Italic', 'Oriya MN','Oriya MN Bold','Oriya Sangam MN','Oriya Sangam MN Bold', 'PT Mono','PT Mono Bold','PT Sans','PT Sans Bold','PT Sans Bold Italic','PT Sans Caption','PT Sans Caption Bold','PT Sans Italic','PT Sans Narrow','PT Sans Narrow Bold','PT Serif','PT Serif Bold','PT Serif Bold Italic','PT Serif Caption','PT Serif Caption Italic','PT Serif Italic', 'Palatino','Palatino Bold','Palatino Bold Italic','Palatino Italic', 'Papyrus','Papyrus Condensed', 'Phosphate Inline','Phosphate Solid', 'PingFang HK','PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight','PingFang SC','PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight','PingFang TC','PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Plantagenet Cherokee', 'Raanana','Raanana Bold', 'Rockwell','Rockwell Bold','Rockwell Bold Italic','Rockwell Italic', 'STIXGeneral','STIXGeneral-Bold','STIXGeneral-BoldItalic','STIXGeneral-Italic','STIXIntegralsD','STIXIntegralsD-Bold','STIXIntegralsSm','STIXIntegralsSm-Bold','STIXIntegralsUp','STIXIntegralsUp-Bold','STIXIntegralsUpD','STIXIntegralsUpD-Bold','STIXIntegralsUpSm','STIXIntegralsUpSm-Bold','STIXNonUnicode','STIXNonUnicode-Bold','STIXNonUnicode-BoldItalic','STIXNonUnicode-Italic','STIXSizeFiveSym','STIXSizeFourSym','STIXSizeFourSym-Bold','STIXSizeOneSym','STIXSizeOneSym-Bold','STIXSizeThreeSym','STIXSizeThreeSym-Bold','STIXSizeTwoSym','STIXSizeTwoSym-Bold','STIXVariants','STIXVariants-Bold', 'STSong', 'Sana', 'Sathu', 'Savoye LET', 'Shree Devanagari 714','Shree Devanagari 714 Bold','Shree Devanagari 714 Bold Italic','Shree Devanagari 714 Italic', 'SignPainter','SignPainter Semibold', 'Silom', 'Sinhala MN','Sinhala MN Bold','Sinhala Sangam MN','Sinhala Sangam MN Bold', 'Skia','Skia Black','Skia Black Condensed','Skia Black Extended','Skia Bold','Skia Condensed','Skia Extended','Skia Light','Skia Light Condensed','Skia Light Extended', 'Snell Roundhand','Snell Roundhand Black','Snell Roundhand Bold', 'Songti SC','Songti SC Black','Songti SC Bold','Songti SC Light','Songti TC','Songti TC Bold','Songti TC Light', 'Sukhumvit Set Bold','Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Semi Bold','Sukhumvit Set Text','Sukhumvit Set Thin', 'Symbol', 'Tahoma','Tahoma Bold', 'Tamil MN','Tamil MN Bold','Tamil Sangam MN','Tamil Sangam MN Bold', 'Telugu MN','Telugu MN Bold','Telugu Sangam MN','Telugu Sangam MN Bold', 'Thonburi','Thonburi Bold','Thonburi Light', 'Times Bold','Times Bold Italic','Times Italic','Times New Roman','Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic','Times Roman', 'Trattatello', 'Trebuchet MS','Trebuchet MS Bold','Trebuchet MS Bold Italic','Trebuchet MS Italic', 'Verdana','Verdana Bold','Verdana Bold Italic','Verdana Italic', 'Waseem','Waseem Light', 'Webdings', 'Wingdings','Wingdings 2','Wingdings 3', 'Zapf Dingbats', 'Zapfino', ] }, 'v12': { name: 'Monterey', version: 12, url: 'https://support.apple.com/en-us/103203', list: [ 'Academy Engraved LET', 'Adelle Sans Devanagari','Adelle Sans Devanagari Bold','Adelle Sans Devanagari Extrabold','Adelle Sans Devanagari Heavy','Adelle Sans Devanagari Light','Adelle Sans Devanagari Semibold','Adelle Sans Devanagari Thin', 'AkayaKanadaka','AkayaTelivigala', 'Al Bayan Bold','Al Bayan','Al Nile','Al Nile Bold','Al Tarikh', 'American Typewriter','American Typewriter Bold','American Typewriter Condensed','American Typewriter Condensed Bold','American Typewriter Condensed Light','American Typewriter Light','American Typewriter Semibold', 'Andale Mono', 'Annai MN', 'Apple Braille','Apple Braille Outline 6 Dot','Apple Braille Outline 8 Dot','Apple Braille Pinpoint 6 Dot','Apple Braille Pinpoint 8 Dot','Apple Chancery','Apple Color Emoji','Apple LiGothic Medium','Apple LiSung Light','Apple SD Gothic Neo','Apple SD Gothic Neo Bold','Apple SD Gothic Neo ExtraBold','Apple SD Gothic Neo Heavy','Apple SD Gothic Neo Light','Apple SD Gothic Neo Medium','Apple SD Gothic Neo SemiBold','Apple SD Gothic Neo Thin','Apple SD Gothic Neo UltraLight','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Black','Arial Bold','Arial Bold Italic','Arial Hebrew','Arial Hebrew Bold','Arial Hebrew Light','Arial Hebrew Scholar','Arial Hebrew Scholar Bold','Arial Hebrew Scholar Light','Arial Italic','Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic','Arial Rounded MT Bold','Arial Unicode MS', 'Arima Koshi','Arima Koshi Black','Arima Koshi Bold','Arima Koshi ExtraBold','Arima Koshi ExtraLight','Arima Koshi Light','Arima Koshi Medium','Arima Koshi Thin','Arima Madurai','Arima Madurai Black','Arima Madurai Bold','Arima Madurai ExtraLight','Arima Madurai Light','Arima Madurai Medium','Arima Madurai Semi Bold','Arima Madurai Thin', 'Avenir Black','Avenir Black Oblique','Avenir Book','Avenir Book Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light','Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Next','Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Condensed','Avenir Next Condensed Bold','Avenir Next Condensed Bold Italic','Avenir Next Condensed Demi Bold','Avenir Next Condensed Demi Bold Italic','Avenir Next Condensed Heavy','Avenir Next Condensed Heavy Italic','Avenir Next Condensed Italic','Avenir Next Condensed Medium','Avenir Next Condensed Medium Italic','Avenir Next Condensed Ultra Light','Avenir Next Condensed Ultra Light Italic','Avenir Next Demi Bold','Avenir Next Demi Bold Italic','Avenir Next Heavy','Avenir Next Heavy Italic','Avenir Next Italic','Avenir Next Medium','Avenir Next Medium Italic','Avenir Next Ultra Light','Avenir Next Ultra Light Italic','Avenir Oblique','Avenir Roman', 'Ayuthaya', 'BM DoHyeon','BM Hanna 11yrs Old','BM Hanna Air','BM Hanna Pro','BM Jua','BM Kirang Haerang','BM Yeonsung', 'Baghdad', 'Bai Jamjuree','Bai Jamjuree Bold','Bai Jamjuree Bold Italic','Bai Jamjuree ExtraLight','Bai Jamjuree ExtraLight Italic','Bai Jamjuree Italic','Bai Jamjuree Light','Bai Jamjuree Light Italic','Bai Jamjuree Medium','Bai Jamjuree Medium Italic','Bai Jamjuree SemiBold','Bai Jamjuree SemiBold Italic', 'Baloo 2','Baloo 2 Bold','Baloo 2 ExtraBold','Baloo 2 Medium','Baloo 2 SemiBold','Baloo Bhai 2','Baloo Bhai 2 Bold','Baloo Bhai 2 ExtraBold','Baloo Bhai 2 Medium','Baloo Bhai 2 SemiBold','Baloo Bhaijaan','Baloo Bhaina 2','Baloo Bhaina 2 Bold','Baloo Bhaina 2 ExtraBold','Baloo Bhaina 2 Medium','Baloo Bhaina 2 SemiBold','Baloo Chettan 2','Baloo Chettan 2 Bold','Baloo Chettan 2 ExtraBold','Baloo Chettan 2 Medium','Baloo Chettan 2 SemiBold','Baloo Da 2','Baloo Da 2 Bold','Baloo Da 2 ExtraBold','Baloo Da 2 Medium','Baloo Da 2 SemiBold','Baloo Paaji 2','Baloo Paaji 2 Bold','Baloo Paaji 2 ExtraBold','Baloo Paaji 2 Medium','Baloo Paaji 2 SemiBold','Baloo Tamma 2','Baloo Tamma 2 Bold','Baloo Tamma 2 ExtraBold','Baloo Tamma 2 Medium','Baloo Tamma 2 SemiBold','Baloo Tammudu 2','Baloo Tammudu 2 Bold','Baloo Tammudu 2 ExtraBold','Baloo Tammudu 2 Medium','Baloo Tammudu 2 SemiBold','Baloo Thambi 2','Baloo Thambi 2 Bold','Baloo Thambi 2 ExtraBold','Baloo Thambi 2 Medium','Baloo Thambi 2 SemiBold', 'Bangla MN','Bangla MN Bold','Bangla Sangam MN','Bangla Sangam MN Bold', 'Baoli SC','Baoli TC', 'Baskerville','Baskerville Bold','Baskerville Bold Italic','Baskerville Italic','Baskerville SemiBold','Baskerville SemiBold Italic', 'Beirut', 'BiauKai', 'Big Caslon Medium', 'Bodoni 72 Bold','Bodoni 72 Book','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book','Bodoni 72 Oldstyle Book Italic','Bodoni 72 Smallcaps Book','Bodoni Ornaments', 'Bradley Hand Bold', 'Brush Script MT Italic', 'Cambay Devanagari','Cambay Devanagari Bold','Cambay Devanagari Bold Oblique','Cambay Devanagari Oblique', 'Canela','Canela Bold','Canela Bold Italic','Canela Deck','Canela Deck Bold','Canela Deck Bold Italic','Canela Deck Medium','Canela Deck Medium Italic','Canela Deck Italic','Canela Italic','Canela Text','Canela Text Bold','Canela Text Bold Italic','Canela Text Medium','Canela Text Medium Italic','Canela Text Italic', 'Catamaran','Catamaran Black','Catamaran Bold','Catamaran ExtraBold','Catamaran ExtraLight','Catamaran Light','Catamaran Medium','Catamaran SemiBold','Catamaran Thin', 'Chakra Petch','Chakra Petch Bold','Chakra Petch Bold Italic','Chakra Petch ExtraLight','Chakra Petch ExtraLight Italic','Chakra Petch Italic','Chakra Petch Light','Chakra Petch Light Italic','Chakra Petch Medium','Chakra Petch Medium Italic','Chakra Petch SemiBold','Chakra Petch SemiBold Italic', 'Chalkboard','Chalkboard Bold','Chalkboard SE','Chalkboard SE Bold','Chalkboard SE Light','Chalkduster', 'Charm','Charm Bold', 'Charmonman','Charmonman Bold', 'Charter Black','Charter Black Italic','Charter Bold','Charter Bold Italic','Charter Italic','Charter Roman', 'Cochin','Cochin Bold','Cochin Bold Italic','Cochin Italic', 'Comic Sans MS','Comic Sans MS Bold', 'Copperplate','Copperplate Bold','Copperplate Light', 'Corsiva Hebrew','Corsiva Hebrew Bold', 'Courier New','Courier New Bold','Courier New Bold Italic','Courier New Italic', 'DIN Alternate Bold','DIN Condensed Bold', 'Damascus','Damascus Bold','Damascus Light','Damascus Medium','Damascus Semi Bold', 'DecoType Naskh', 'Devanagari MT','Devanagari MT Bold','Devanagari Sangam MN','Devanagari Sangam MN Bold', 'Didot','Didot Bold','Didot Italic', 'Diwan Kufi','Diwan Thuluth', 'Domaine Display','Domaine Display Bold','Domaine Display Bold Italic','Domaine Display Italic','Domaine Display Medium','Domaine Display Medium Italic', 'Euphemia UCAS','Euphemia UCAS Bold','Euphemia UCAS Italic', 'Fahkwang','Fahkwang Bold','Fahkwang Bold Italic','Fahkwang ExtraLight','Fahkwang ExtraLight Italic','Fahkwang Italic','Fahkwang Light','Fahkwang Light Italic','Fahkwang Medium','Fahkwang Medium Italic','Fahkwang SemiBold','Fahkwang SemiBold Italic', 'Farah', 'Farisi', 'Founders Grotesk','Founders Grotesk Bold','Founders Grotesk Bold Italic','Founders Grotesk Condensed','Founders Grotesk Condensed Bold','Founders Grotesk Condensed Semibold','Founders Grotesk Light','Founders Grotesk Light Italic','Founders Grotesk Medium','Founders Grotesk Medium Italic','Founders Grotesk Italic','Founders Grotesk Semibold','Founders Grotesk Semibold Italic','Founders Grotesk Text','Founders Grotesk Text Bold','Founders Grotesk Text Bold Italic','Founders Grotesk Text Italic', 'Futura Bold','Futura Condensed ExtraBold','Futura Condensed Medium','Futura Medium','Futura Medium Italic', 'GB18030 Bitmap', 'Galvji','Galvji Bold','Galvji Bold Oblique','Galvji Oblique', 'Geeza Pro','Geeza Pro Bold', 'Geneva', 'Georgia','Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Gill Sans','Gill Sans Bold','Gill Sans Bold Italic','Gill Sans Italic','Gill Sans Light','Gill Sans Light Italic','Gill Sans SemiBold','Gill Sans SemiBold Italic','Gill Sans UltraBold', 'Gotu', 'Grantha Sangam MN','Grantha Sangam MN Black','Grantha Sangam MN Bold','Grantha Sangam MN DemiBold','Grantha Sangam MN Light','Grantha Sangam MN Medium', 'Graphik','Graphik Bold','Graphik Bold Italic','Graphik Compact','Graphik Compact Bold','Graphik Compact Bold Italic','Graphik Compact Medium','Graphik Compact Medium Italic','Graphik Compact Italic','Graphik Compact Semibold','Graphik Compact Semibold Italic','Graphik Light','Graphik Light Italic','Graphik Medium','Graphik Medium Italic','Graphik Italic','Graphik Semibold','Graphik Semibold Italic', 'Gujarati MT','Gujarati MT Bold','Gujarati Sangam MN','Gujarati Sangam MN Bold', 'GungSeo', 'Gurmukhi MN','Gurmukhi MN Bold','Gurmukhi MT','Gurmukhi Sangam MN','Gurmukhi Sangam MN Bold', 'Hannotate SC','Hannotate SC Bold','Hannotate TC','Hannotate TC Bold', 'HanziPen SC','HanziPen SC Bold','HanziPen TC','HanziPen TC Bold', 'HeadLineA', 'Hei', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Helvetica','Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Neue','Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Condensed Black','Helvetica Neue Condensed Bold','Helvetica Neue Italic','Helvetica Neue Light','Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin','Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic','Helvetica Oblique', 'Herculanum', 'Hiragino Maru Gothic ProN W4','Hiragino Mincho ProN W3','Hiragino Mincho ProN W6','Hiragino Sans CNS W3','Hiragino Sans CNS W6','Hiragino Sans GB W3','Hiragino Sans GB W6','Hiragino Sans W0','Hiragino Sans W1','Hiragino Sans W2','Hiragino Sans W3','Hiragino Sans W4','Hiragino Sans W5','Hiragino Sans W6','Hiragino Sans W7','Hiragino Sans W8','Hiragino Sans W9', 'Hoefler Text','Hoefler Text Black','Hoefler Text Black Italic','Hoefler Text Italic','Hoefler Text Ornaments', 'Hubballi', 'ITF Devanagari Bold','ITF Devanagari Book','ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Marathi Bold','ITF Devanagari Marathi Book','ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium','ITF Devanagari Medium', 'Impact', 'InaiMathi','InaiMathi Bold', 'Jaini','Jaini Purva', 'K2D','K2D Bold','K2D Bold Italic','K2D ExtraBold','K2D ExtraBold Italic','K2D ExtraLight','K2D ExtraLight Italic','K2D Italic','K2D Light','K2D Light Italic','K2D Medium','K2D Medium Italic','K2D SemiBold','K2D SemiBold Italic','K2D Thin','K2D Thin Italic', 'Kai', 'Kailasa','Kailasa Bold', 'Kaiti SC','Kaiti SC Black','Kaiti SC Bold','Kaiti TC','Kaiti TC Black','Kaiti TC Bold', 'Kannada MN','Kannada MN Bold','Kannada Sangam MN','Kannada Sangam MN Bold', 'Katari','Katari Black','Katari Black Italic','Katari Bold','Katari Bold Italic','Katari Italic','Katari Medium','Katari Medium Italic', 'Kavivanar', 'Kefa','Kefa Bold', 'Khmer MN','Khmer MN Bold','Khmer Sangam MN', 'Klee Demibold','Klee Medium', 'KoHo','KoHo Bold','KoHo Bold Italic','KoHo ExtraLight','KoHo ExtraLight Italic','KoHo Italic','KoHo Light','KoHo Light Italic','KoHo Medium','KoHo Medium Italic','KoHo SemiBold','KoHo SemiBold Italic', 'Kodchasan','Kodchasan Bold','Kodchasan Bold Italic','Kodchasan ExtraLight','Kodchasan ExtraLight Italic','Kodchasan Italic','Kodchasan Light','Kodchasan Light Italic','Kodchasan Medium','Kodchasan Medium Italic','Kodchasan SemiBold','Kodchasan SemiBold Italic', 'Kohinoor Bangla','Kohinoor Bangla Bold','Kohinoor Bangla Light','Kohinoor Bangla Medium','Kohinoor Bangla Semibold','Kohinoor Devanagari','Kohinoor Devanagari Bold','Kohinoor Devanagari Light','Kohinoor Devanagari Medium','Kohinoor Devanagari Semibold','Kohinoor Gujarati','Kohinoor Gujarati Bold','Kohinoor Gujarati Light','Kohinoor Gujarati Medium','Kohinoor Gujarati Semibold','Kohinoor Telugu','Kohinoor Telugu Bold','Kohinoor Telugu Light','Kohinoor Telugu Medium','Kohinoor Telugu Semibold', 'Kokonor', 'Krub','Krub Bold','Krub Bold Italic','Krub ExtraLight','Krub ExtraLight Italic','Krub Italic','Krub Light','Krub Light Italic','Krub Medium','Krub Medium Italic','Krub SemiBold','Krub SemiBold Italic', 'Krungthep', 'KufiStandardGK', 'Lahore Gurmukhi','Lahore Gurmukhi Bold','Lahore Gurmukhi Light','Lahore Gurmukhi Medium','Lahore Gurmukhi SemiBold', 'Lantinghei SC Demibold','Lantinghei SC Extralight','Lantinghei SC Heavy','Lantinghei TC Demibold','Lantinghei TC Heavy', 'Lao MN','Lao MN Bold','Lao Sangam MN', 'Lava Devanagari','Lava Devanagari Bold','Lava Devanagari Heavy','Lava Devanagari Medium','Lava Kannada','Lava Kannada Bold','Lava Kannada Heavy','Lava Kannada Medium','Lava Telugu','Lava Telugu Bold','Lava Telugu Heavy','Lava Telugu Medium', 'LiHei Pro', 'LiSong Pro', 'Libian SC','Libian TC', 'LingWai SC Medium','LingWai TC Medium', 'Lucida Grande','Lucida Grande Bold', 'Luminari', 'Maku','Maku Bold', 'Malayalam MN','Malayalam MN Bold','Malayalam Sangam MN','Malayalam Sangam MN Bold', 'Mali','Mali Bold','Mali Bold Italic','Mali ExtraLight','Mali ExtraLight Italic','Mali Italic','Mali Light','Mali Light Italic','Mali Medium','Mali Medium Italic','Mali SemiBold','Mali SemiBold Italic', 'Marker Felt Thin','Marker Felt Wide', 'Menlo','Menlo Bold','Menlo Bold Italic','Menlo Italic', 'Microsoft Sans Serif', 'Mishafi','Mishafi Gold', 'Modak', 'Monaco', 'Mshtakan','Mshtakan Bold','Mshtakan BoldOblique','Mshtakan Oblique', 'Mukta','Mukta Bold','Mukta ExtraBold','Mukta ExtraLight','Mukta Light','Mukta Malar','Mukta Malar Bold','Mukta Malar ExtraBold','Mukta Malar ExtraLight','Mukta Malar Light','Mukta Malar Medium','Mukta Malar SemiBold','Mukta Medium','Mukta SemiBold','Mukta Vaani','Mukta Vaani Bold','Mukta Vaani ExtraBold','Mukta Vaani ExtraLight','Mukta Vaani Light','Mukta Vaani Medium','Mukta Vaani SemiBold', 'Mukta Mahee','Mukta Mahee Bold','Mukta Mahee ExtraBold','Mukta Mahee ExtraLight','Mukta Mahee Light','Mukta Mahee Medium','Mukta Mahee SemiBold', 'Muna','Muna Black','Muna Bold', 'Myanmar MN','Myanmar MN Bold','Myanmar Sangam MN','Myanmar Sangam MN Bold', 'Myriad Arabic','Myriad Arabic Black','Myriad Arabic Black Italic','Myriad Arabic Bold','Myriad Arabic Bold Italic','Myriad Arabic Italic','Myriad Arabic Light','Myriad Arabic Light Italic','Myriad Arabic Semibold','Myriad Arabic Semibold Italic', 'Nadeem', 'Nanum Brush Script','Nanum Pen Script','Nanum Gothic','Nanum Gothic Bold','Nanum Gothic ExtraBold','Nanum Myeongjo','Nanum Myeongjo Bold','Nanum Myeongjo ExtraBold', 'New Peninim MT','New Peninim MT Bold','New Peninim MT Bold Inclined','New Peninim MT Inclined', 'Niramit','Niramit Bold','Niramit Bold Italic','Niramit ExtraLight','Niramit ExtraLight Italic','Niramit Italic','Niramit Light','Niramit Light Italic','Niramit Medium','Niramit Medium Italic','Niramit SemiBold','Niramit SemiBold Italic', 'Noteworthy Bold','Noteworthy Light', 'Noto Nastaliq Urdu','Noto Nastaliq Urdu Bold', 'Noto Sans Kannada','Noto Sans Kannada Black','Noto Sans Kannada Bold','Noto Sans Kannada ExtraBold','Noto Sans Kannada ExtraLight','Noto Sans Kannada Light','Noto Sans Kannada Medium','Noto Sans Kannada SemiBold','Noto Sans Kannada Thin','Noto Sans Myanmar','Noto Sans Myanmar Black','Noto Sans Myanmar Bold','Noto Sans Myanmar ExtraBold','Noto Sans Myanmar ExtraLight','Noto Sans Myanmar Light','Noto Sans Myanmar Medium','Noto Sans Myanmar SemiBold','Noto Sans Myanmar Thin','Noto Sans Oriya','Noto Sans Oriya Bold', 'Noto Serif Kannada','Noto Serif Kannada Black','Noto Serif Kannada Bold','Noto Serif Kannada ExtraBold','Noto Serif Kannada ExtraLight','Noto Serif Kannada Light','Noto Serif Kannada Medium','Noto Serif Kannada SemiBold','Noto Serif Kannada Thin','Noto Serif Myanmar','Noto Serif Myanmar Black','Noto Serif Myanmar Bold','Noto Serif Myanmar ExtraBold','Noto Serif Myanmar ExtraLight','Noto Serif Myanmar Light','Noto Serif Myanmar Medium','Noto Serif Myanmar SemiBold','Noto Serif Myanmar Thin', 'October Compressed Devanagari','October Compressed Devanagari Black','October Compressed Devanagari Bold','October Compressed Devanagari ExtraLight','October Compressed Devanagari Hairline','October Compressed Devanagari Heavy','October Compressed Devanagari Light','October Compressed Devanagari Medium','October Compressed Devanagari Thin','October Compressed Tamil','October Compressed Tamil Black','October Compressed Tamil Bold','October Compressed Tamil ExtraLight','October Compressed Tamil Hairline','October Compressed Tamil Heavy','October Compressed Tamil Light','October Compressed Tamil Medium','October Compressed Tamil Thin','October Condensed Devanagari','October Condensed Devanagari Black','October Condensed Devanagari Bold','October Condensed Devanagari ExtraLight','October Condensed Devanagari Hairline','October Condensed Devanagari Heavy','October Condensed Devanagari Light','October Condensed Devanagari Medium','October Condensed Devanagari Thin','October Condensed Tamil','October Condensed Tamil Black','October Condensed Tamil Bold','October Condensed Tamil ExtraLight','October Condensed Tamil Hairline','October Condensed Tamil Heavy','October Condensed Tamil Light','October Condensed Tamil Medium','October Condensed Tamil Thin','October Devanagari','October Devanagari Black','October Devanagari Bold','October Devanagari ExtraLight','October Devanagari Hairline','October Devanagari Heavy','October Devanagari Light','October Devanagari Medium','October Devanagari Thin','October Tamil','October Tamil Black','October Tamil Bold','October Tamil ExtraLight','October Tamil Hairline','October Tamil Heavy','October Tamil Light','October Tamil Medium','October Tamil Thin', 'Optima','Optima Bold','Optima Bold Italic','Optima ExtraBlack','Optima Italic', 'Oriya MN','Oriya MN Bold','Oriya Sangam MN','Oriya Sangam MN Bold', 'Osaka','Osaka-Mono', 'PCMyungjo', 'PSL Ornanong Pro','PSL Ornanong Pro Bold','PSL Ornanong Pro Bold Italic','PSL Ornanong Pro Demibold','PSL Ornanong Pro Demibold Italic','PSL Ornanong Pro Italic','PSL Ornanong Pro Light','PSL Ornanong Pro Light Italic', 'PT Mono','PT Mono Bold','PT Sans','PT Sans Bold','PT Sans Bold Italic','PT Sans Caption','PT Sans Caption Bold','PT Sans Italic','PT Sans Narrow','PT Sans Narrow Bold','PT Serif','PT Serif Bold','PT Serif Bold Italic','PT Serif Caption','PT Serif Caption Italic','PT Serif Italic', 'Padyakke Expanded One', 'Palatino','Palatino Bold','Palatino Bold Italic','Palatino Italic', 'Papyrus','Papyrus Condensed', 'Party LET', 'Phosphate Inline','Phosphate Solid', 'PilGi', 'PingFang HK','PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight','PingFang SC','PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight','PingFang TC','PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Plantagenet Cherokee', 'Produkt','Produkt Extralight','Produkt Extralight Italic','Produkt Light','Produkt Light Italic','Produkt Medium','Produkt Medium Italic','Produkt Italic', 'Proxima Nova','Proxima Nova Bold','Proxima Nova Bold It','Proxima Nova Extrabold','Proxima Nova Extrabold It','Proxima Nova It','Proxima Nova Light','Proxima Nova Light It','Proxima Nova Medium','Proxima Nova Medium It','Proxima Nova Semibold','Proxima Nova Semibold It', 'Publico Headline Black','Publico Headline Black Italic','Publico Headline Bold','Publico Headline Bold Italic','Publico Headline Italic','Publico Headline Roman','Publico Text Bold','Publico Text Bold Italic','Publico Text Italic','Publico Text Roman','Publico Text Semibold','Publico Text Semibold Italic', 'Quotes Caps','Quotes Script', 'Raanana','Raanana Bold', 'Rockwell','Rockwell Bold','Rockwell Bold Italic','Rockwell Italic', 'STFangsong', 'STHeiti', 'STIX Two Math','STIX Two Text','STIX Two Text Bold','STIX Two Text Bold Italic','STIX Two Text Italic','STIXGeneral','STIXGeneral-Bold','STIXGeneral-BoldItalic','STIXGeneral-Italic','STIXIntegralsD','STIXIntegralsD-Bold','STIXIntegralsSm','STIXIntegralsSm-Bold','STIXIntegralsUp','STIXIntegralsUp-Bold','STIXIntegralsUpD','STIXIntegralsUpD-Bold','STIXIntegralsUpSm','STIXIntegralsUpSm-Bold','STIXNonUnicode','STIXNonUnicode-Bold','STIXNonUnicode-BoldItalic','STIXNonUnicode-Italic','STIXSizeFiveSym','STIXSizeFourSym','STIXSizeFourSym-Bold','STIXSizeOneSym','STIXSizeOneSym-Bold','STIXSizeThreeSym','STIXSizeThreeSym-Bold','STIXSizeTwoSym','STIXSizeTwoSym-Bold','STIXVariants','STIXVariants-Bold', 'STKaiti', 'STSong', 'STXihei', 'Sama Devanagari','Sama Devanagari Bold','Sama Devanagari Book','Sama Devanagari ExtraBold','Sama Devanagari Medium','Sama Devanagari SemiBold','Sama Gujarati','Sama Gujarati Bold','Sama Gujarati Book','Sama Gujarati ExtraBold','Sama Gujarati Medium','Sama Gujarati SemiBold','Sama Gurmukhi','Sama Gurmukhi Bold','Sama Gurmukhi Book','Sama Gurmukhi ExtraBold','Sama Gurmukhi Medium','Sama Gurmukhi SemiBold','Sama Kannada','Sama Kannada Bold','Sama Kannada Book','Sama Kannada ExtraBold','Sama Kannada Medium','Sama Kannada SemiBold','Sama Malayalam','Sama Malayalam Bold','Sama Malayalam Book','Sama Malayalam ExtraBold','Sama Malayalam Medium','Sama Malayalam SemiBold','Sama Tamil','Sama Tamil Bold','Sama Tamil Book','Sama Tamil ExtraBold','Sama Tamil Medium','Sama Tamil SemiBold', 'Sana', 'Sarabun','Sarabun Bold','Sarabun Bold Italic','Sarabun ExtraBold','Sarabun ExtraBold Italic','Sarabun ExtraLight','Sarabun ExtraLight Italic','Sarabun Italic','Sarabun Light','Sarabun Light Italic','Sarabun Medium','Sarabun Medium Italic','Sarabun SemiBold','Sarabun SemiBold Italic','Sarabun Thin','Sarabun Thin Italic', 'Sathu', 'Sauber Script', 'Savoye LET', 'Shobhika','Shobhika Bold', 'Shree Devanagari 714','Shree Devanagari 714 Bold','Shree Devanagari 714 Bold Italic','Shree Devanagari 714 Italic', 'SignPainter','SignPainter Semibold', 'Silom', 'SimSong','SimSong Bold', 'Sinhala MN','Sinhala MN Bold','Sinhala Sangam MN','Sinhala Sangam MN Bold', 'Skia','Skia Black','Skia Black Condensed','Skia Black Extended','Skia Bold','Skia Condensed','Skia Extended','Skia Light','Skia Light Condensed','Skia Light Extended', 'Snell Roundhand','Snell Roundhand Black','Snell Roundhand Bold', 'Songti SC','Songti SC Black','Songti SC Bold','Songti SC Light','Songti TC','Songti TC Bold','Songti TC Light', 'Spot Mono','Spot Mono Bold','Spot Mono Medium', 'Srisakdi','Srisakdi Bold', 'Sukhumvit Set Bold','Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Semi Bold','Sukhumvit Set Text','Sukhumvit Set Thin', 'Symbol', 'Tahoma','Tahoma Bold', 'Tamil MN','Tamil MN Bold','Tamil Sangam MN','Tamil Sangam MN Black','Tamil Sangam MN Bold','Tamil Sangam MN Demibold','Tamil Sangam MN Light','Tamil Sangam MN Medium', 'Telugu MN','Telugu MN Bold','Telugu Sangam MN','Telugu Sangam MN Bold', 'Thonburi','Thonburi Bold','Thonburi Light', 'Times New Roman','Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Tiro Bangla','Tiro Bangla Italic','Tiro Devanagari Hindi','Tiro Devanagari Hindi Italic','Tiro Devanagari Marathi','Tiro Devanagari Marathi Italic','Tiro Devanagari Sanskrit','Tiro Devanagari Sanskrit Italic','Tiro Gurmukhi','Tiro Gurmukhi Italic','Tiro Kannada','Tiro Kannada Italic','Tiro Tamil','Tiro Tamil Italic','Tiro Telugu','Tiro Telugu Italic', 'Toppan Bunkyu Gothic','Toppan Bunkyu Gothic Demibold','Toppan Bunkyu Midashi Gothic Extrabold','Toppan Bunkyu Midashi Mincho Extrabold','Toppan Bunkyu Mincho', 'Trattatello', 'Trebuchet MS','Trebuchet MS Bold','Trebuchet MS Bold Italic','Trebuchet MS Italic', 'Tsukushi A Round Gothic','Tsukushi A Round Gothic Bold','Tsukushi B Round Gothic','Tsukushi B Round Gothic Bold', 'Verdana','Verdana Bold','Verdana Bold Italic','Verdana Italic', 'Waseem','Waseem Light', 'Wawati SC','Wawati TC', 'Webdings', 'Weibei SC Bold','Weibei TC Bold', 'Wingdings','Wingdings 2','Wingdings 3', 'Xingkai SC Bold','Xingkai SC Light','Xingkai TC Bold','Xingkai TC Light', 'YuGothic Bold','YuGothic Medium', 'YuKyokasho Bold','YuKyokasho Medium','YuKyokasho Yoko Bold','YuKyokasho Yoko Medium', 'YuMincho +36p Kana Demibold','YuMincho +36p Kana Extrabold','YuMincho +36p Kana Medium','YuMincho Demibold','YuMincho Extrabold','YuMincho Medium', 'Yuanti SC','Yuanti SC Bold','Yuanti SC Light','Yuanti TC','Yuanti TC Bold','Yuanti TC Light', 'Yuppy SC','Yuppy TC', 'Zapf Dingbats', 'Zapfino', ] }, 'v13': { name: 'Ventura', version: 13, url: 'https://support.apple.com/en-us/103197', list: [ 'Academy Engraved LET', 'Adelle Sans Devanagari','Adelle Sans Devanagari Bold','Adelle Sans Devanagari Extrabold','Adelle Sans Devanagari Heavy','Adelle Sans Devanagari Light','Adelle Sans Devanagari Semibold','Adelle Sans Devanagari Thin', 'AkayaKanadaka','AkayaTelivigala', 'Al Bayan Bold','Al Bayan','Al Nile','Al Nile Bold','Al Tarikh', 'American Typewriter','American Typewriter Bold','American Typewriter Condensed','American Typewriter Condensed Bold','American Typewriter Condensed Light','American Typewriter Light','American Typewriter Semibold', 'Andale Mono', 'Annai MN', 'Apple Braille','Apple Braille Outline 6 Dot','Apple Braille Outline 8 Dot','Apple Braille Pinpoint 6 Dot','Apple Braille Pinpoint 8 Dot','Apple Chancery','Apple Color Emoji','Apple LiGothic Medium','Apple LiSung Light','Apple SD Gothic Neo','Apple SD Gothic Neo Bold','Apple SD Gothic Neo ExtraBold','Apple SD Gothic Neo Heavy','Apple SD Gothic Neo Light','Apple SD Gothic Neo Medium','Apple SD Gothic Neo SemiBold','Apple SD Gothic Neo Thin','Apple SD Gothic Neo UltraLight','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Black','Arial Bold','Arial Bold Italic','Arial Hebrew','Arial Hebrew Bold','Arial Hebrew Light','Arial Hebrew Scholar','Arial Hebrew Scholar Bold','Arial Hebrew Scholar Light','Arial Italic','Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic','Arial Rounded MT Bold','Arial Unicode MS', 'Arima Koshi','Arima Koshi Black','Arima Koshi Bold','Arima Koshi ExtraBold','Arima Koshi ExtraLight','Arima Koshi Light','Arima Koshi Medium','Arima Koshi Thin','Arima Madurai','Arima Madurai Black','Arima Madurai Bold','Arima Madurai ExtraLight','Arima Madurai Light','Arima Madurai Medium','Arima Madurai Semi Bold','Arima Madurai Thin', 'Avenir Black','Avenir Black Oblique','Avenir Book','Avenir Book Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light','Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Next','Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Condensed','Avenir Next Condensed Bold','Avenir Next Condensed Bold Italic','Avenir Next Condensed Demi Bold','Avenir Next Condensed Demi Bold Italic','Avenir Next Condensed Heavy','Avenir Next Condensed Heavy Italic','Avenir Next Condensed Italic','Avenir Next Condensed Medium','Avenir Next Condensed Medium Italic','Avenir Next Condensed Ultra Light','Avenir Next Condensed Ultra Light Italic','Avenir Next Demi Bold','Avenir Next Demi Bold Italic','Avenir Next Heavy','Avenir Next Heavy Italic','Avenir Next Italic','Avenir Next Medium','Avenir Next Medium Italic','Avenir Next Ultra Light','Avenir Next Ultra Light Italic','Avenir Oblique','Avenir Roman', 'Ayuthaya', 'BM DoHyeon','BM Hanna 11yrs Old','BM Hanna Air','BM Hanna Pro','BM Jua','BM Kirang Haerang','BM Yeonsung', 'Baghdad', 'Bai Jamjuree','Bai Jamjuree Bold','Bai Jamjuree Bold Italic','Bai Jamjuree ExtraLight','Bai Jamjuree ExtraLight Italic','Bai Jamjuree Italic','Bai Jamjuree Light','Bai Jamjuree Light Italic','Bai Jamjuree Medium','Bai Jamjuree Medium Italic','Bai Jamjuree SemiBold','Bai Jamjuree SemiBold Italic', 'Baloo 2','Baloo 2 Bold','Baloo 2 ExtraBold','Baloo 2 Medium','Baloo 2 SemiBold','Baloo Bhai 2','Baloo Bhai 2 Bold','Baloo Bhai 2 ExtraBold','Baloo Bhai 2 Medium','Baloo Bhai 2 SemiBold','Baloo Bhaijaan','Baloo Bhaina 2','Baloo Bhaina 2 Bold','Baloo Bhaina 2 ExtraBold','Baloo Bhaina 2 Medium','Baloo Bhaina 2 SemiBold','Baloo Chettan 2','Baloo Chettan 2 Bold','Baloo Chettan 2 ExtraBold','Baloo Chettan 2 Medium','Baloo Chettan 2 SemiBold','Baloo Da 2','Baloo Da 2 Bold','Baloo Da 2 ExtraBold','Baloo Da 2 Medium','Baloo Da 2 SemiBold','Baloo Paaji 2','Baloo Paaji 2 Bold','Baloo Paaji 2 ExtraBold','Baloo Paaji 2 Medium','Baloo Paaji 2 SemiBold','Baloo Tamma 2','Baloo Tamma 2 Bold','Baloo Tamma 2 ExtraBold','Baloo Tamma 2 Medium','Baloo Tamma 2 SemiBold','Baloo Tammudu 2','Baloo Tammudu 2 Bold','Baloo Tammudu 2 ExtraBold','Baloo Tammudu 2 Medium','Baloo Tammudu 2 SemiBold','Baloo Thambi 2','Baloo Thambi 2 Bold','Baloo Thambi 2 ExtraBold','Baloo Thambi 2 Medium','Baloo Thambi 2 SemiBold', 'Bangla MN','Bangla MN Bold','Bangla Sangam MN','Bangla Sangam MN Bold', 'Baoli SC','Baoli TC', 'Baskerville','Baskerville Bold','Baskerville Bold Italic','Baskerville Italic','Baskerville SemiBold','Baskerville SemiBold Italic', 'Beirut', 'BiauKai', 'Big Caslon Medium', 'Bodoni 72 Bold','Bodoni 72 Book','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book','Bodoni 72 Oldstyle Book Italic','Bodoni 72 Smallcaps Book','Bodoni Ornaments', 'Bradley Hand Bold', 'Brush Script MT Italic', 'Cambay Devanagari','Cambay Devanagari Bold','Cambay Devanagari Bold Oblique','Cambay Devanagari Oblique', 'Canela','Canela Bold','Canela Bold Italic','Canela Deck','Canela Deck Bold','Canela Deck Bold Italic','Canela Deck Medium','Canela Deck Medium Italic','Canela Deck Italic','Canela Italic','Canela Text','Canela Text Bold','Canela Text Bold Italic','Canela Text Medium','Canela Text Medium Italic','Canela Text Italic', 'Catamaran','Catamaran Black','Catamaran Bold','Catamaran ExtraBold','Catamaran ExtraLight','Catamaran Light','Catamaran Medium','Catamaran SemiBold','Catamaran Thin', 'Chakra Petch','Chakra Petch Bold','Chakra Petch Bold Italic','Chakra Petch ExtraLight','Chakra Petch ExtraLight Italic','Chakra Petch Italic','Chakra Petch Light','Chakra Petch Light Italic','Chakra Petch Medium','Chakra Petch Medium Italic','Chakra Petch SemiBold','Chakra Petch SemiBold Italic', 'Chalkboard','Chalkboard Bold','Chalkboard SE','Chalkboard SE Bold','Chalkboard SE Light','Chalkduster', 'Charm','Charm Bold', 'Charmonman','Charmonman Bold', 'Charter Black','Charter Black Italic','Charter Bold','Charter Bold Italic','Charter Italic','Charter Roman', 'Cochin','Cochin Bold','Cochin Bold Italic','Cochin Italic', 'Comic Sans MS','Comic Sans MS Bold', 'Copperplate','Copperplate Bold','Copperplate Light', 'Corsiva Hebrew','Corsiva Hebrew Bold', 'Courier New','Courier New Bold','Courier New Bold Italic','Courier New Italic', 'DIN Alternate Bold','DIN Condensed Bold', 'Damascus','Damascus Bold','Damascus Light','Damascus Medium','Damascus Semi Bold', 'DecoType Naskh', 'Devanagari MT','Devanagari MT Bold','Devanagari Sangam MN','Devanagari Sangam MN Bold', 'Didot','Didot Bold','Didot Italic', 'Diwan Kufi','Diwan Thuluth', 'Domaine Display','Domaine Display Bold','Domaine Display Bold Italic','Domaine Display Italic','Domaine Display Medium','Domaine Display Medium Italic', 'Euphemia UCAS','Euphemia UCAS Bold','Euphemia UCAS Italic', 'Fahkwang','Fahkwang Bold','Fahkwang Bold Italic','Fahkwang ExtraLight','Fahkwang ExtraLight Italic','Fahkwang Italic','Fahkwang Light','Fahkwang Light Italic','Fahkwang Medium','Fahkwang Medium Italic','Fahkwang SemiBold','Fahkwang SemiBold Italic', 'Farah', 'Farisi', 'Founders Grotesk','Founders Grotesk Bold','Founders Grotesk Bold Italic','Founders Grotesk Condensed','Founders Grotesk Condensed Bold','Founders Grotesk Condensed Semibold','Founders Grotesk Light','Founders Grotesk Light Italic','Founders Grotesk Medium','Founders Grotesk Medium Italic','Founders Grotesk Italic','Founders Grotesk Semibold','Founders Grotesk Semibold Italic','Founders Grotesk Text','Founders Grotesk Text Bold','Founders Grotesk Text Bold Italic','Founders Grotesk Text Italic', 'Futura Bold','Futura Condensed ExtraBold','Futura Condensed Medium','Futura Medium','Futura Medium Italic', 'GB18030 Bitmap', 'Galvji','Galvji Bold','Galvji Bold Oblique','Galvji Oblique', 'Geeza Pro','Geeza Pro Bold', 'Geneva', 'Georgia','Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Gill Sans','Gill Sans Bold','Gill Sans Bold Italic','Gill Sans Italic','Gill Sans Light','Gill Sans Light Italic','Gill Sans SemiBold','Gill Sans SemiBold Italic','Gill Sans UltraBold', 'Gotu', 'Grantha Sangam MN','Grantha Sangam MN Black','Grantha Sangam MN Bold','Grantha Sangam MN DemiBold','Grantha Sangam MN Light','Grantha Sangam MN Medium', 'Graphik','Graphik Bold','Graphik Bold Italic','Graphik Compact','Graphik Compact Bold','Graphik Compact Bold Italic','Graphik Compact Medium','Graphik Compact Medium Italic','Graphik Compact Italic','Graphik Compact Semibold','Graphik Compact Semibold Italic','Graphik Light','Graphik Light Italic','Graphik Medium','Graphik Medium Italic','Graphik Italic','Graphik Semibold','Graphik Semibold Italic', 'Gujarati MT','Gujarati MT Bold','Gujarati Sangam MN','Gujarati Sangam MN Bold', 'GungSeo', 'Gurmukhi MN','Gurmukhi MN Bold','Gurmukhi MT','Gurmukhi Sangam MN','Gurmukhi Sangam MN Bold', 'Hannotate SC','Hannotate SC Bold','Hannotate TC','Hannotate TC Bold', 'HanziPen SC','HanziPen SC Bold','HanziPen TC','HanziPen TC Bold', 'HeadLineA', 'Hei', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Helvetica','Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Neue','Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Condensed Black','Helvetica Neue Condensed Bold','Helvetica Neue Italic','Helvetica Neue Light','Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin','Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic','Helvetica Oblique', 'Herculanum', 'Hiragino Maru Gothic ProN W4','Hiragino Mincho ProN W3','Hiragino Mincho ProN W6','Hiragino Sans CNS W3','Hiragino Sans CNS W6','Hiragino Sans GB W3','Hiragino Sans GB W6','Hiragino Sans W0','Hiragino Sans W1','Hiragino Sans W2','Hiragino Sans W3','Hiragino Sans W4','Hiragino Sans W5','Hiragino Sans W6','Hiragino Sans W7','Hiragino Sans W8','Hiragino Sans W9', 'Hoefler Text','Hoefler Text Black','Hoefler Text Black Italic','Hoefler Text Italic','Hoefler Text Ornaments', 'Hubballi', 'ITF Devanagari Bold','ITF Devanagari Book','ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Marathi Bold','ITF Devanagari Marathi Book','ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium','ITF Devanagari Medium', 'Impact', 'InaiMathi','InaiMathi Bold', 'Jaini','Jaini Purva', 'K2D','K2D Bold','K2D Bold Italic','K2D ExtraBold','K2D ExtraBold Italic','K2D ExtraLight','K2D ExtraLight Italic','K2D Italic','K2D Light','K2D Light Italic','K2D Medium','K2D Medium Italic','K2D SemiBold','K2D SemiBold Italic','K2D Thin','K2D Thin Italic', 'Kai', 'Kailasa','Kailasa Bold', 'Kaiti SC','Kaiti SC Black','Kaiti SC Bold','Kaiti TC','Kaiti TC Black','Kaiti TC Bold', 'Kannada MN','Kannada MN Bold','Kannada Sangam MN','Kannada Sangam MN Bold', 'Katari','Katari Black','Katari Black Italic','Katari Bold','Katari Bold Italic','Katari Italic','Katari Medium','Katari Medium Italic', 'Kavivanar', 'Kefa','Kefa Bold', 'Khmer MN','Khmer MN Bold','Khmer Sangam MN', 'Klee Demibold','Klee Medium', 'KoHo','KoHo Bold','KoHo Bold Italic','KoHo ExtraLight','KoHo ExtraLight Italic','KoHo Italic','KoHo Light','KoHo Light Italic','KoHo Medium','KoHo Medium Italic','KoHo SemiBold','KoHo SemiBold Italic', 'Kodchasan','Kodchasan Bold','Kodchasan Bold Italic','Kodchasan ExtraLight','Kodchasan ExtraLight Italic','Kodchasan Italic','Kodchasan Light','Kodchasan Light Italic','Kodchasan Medium','Kodchasan Medium Italic','Kodchasan SemiBold','Kodchasan SemiBold Italic', 'Kohinoor Bangla','Kohinoor Bangla Bold','Kohinoor Bangla Light','Kohinoor Bangla Medium','Kohinoor Bangla Semibold','Kohinoor Devanagari','Kohinoor Devanagari Bold','Kohinoor Devanagari Light','Kohinoor Devanagari Medium','Kohinoor Devanagari Semibold','Kohinoor Gujarati','Kohinoor Gujarati Bold','Kohinoor Gujarati Light','Kohinoor Gujarati Medium','Kohinoor Gujarati Semibold','Kohinoor Telugu','Kohinoor Telugu Bold','Kohinoor Telugu Light','Kohinoor Telugu Medium','Kohinoor Telugu Semibold', 'Kokonor', 'Krub','Krub Bold','Krub Bold Italic','Krub ExtraLight','Krub ExtraLight Italic','Krub Italic','Krub Light','Krub Light Italic','Krub Medium','Krub Medium Italic','Krub SemiBold','Krub SemiBold Italic', 'Krungthep', 'KufiStandardGK', 'Lahore Gurmukhi','Lahore Gurmukhi Bold','Lahore Gurmukhi Light','Lahore Gurmukhi Medium','Lahore Gurmukhi SemiBold', 'Lantinghei SC Demibold','Lantinghei SC Extralight','Lantinghei SC Heavy','Lantinghei TC Demibold','Lantinghei TC Heavy', 'Lao MN','Lao MN Bold','Lao Sangam MN', 'Lava Devanagari','Lava Devanagari Bold','Lava Devanagari Heavy','Lava Devanagari Medium','Lava Kannada','Lava Kannada Bold','Lava Kannada Heavy','Lava Kannada Medium','Lava Telugu','Lava Telugu Bold','Lava Telugu Heavy','Lava Telugu Medium', 'LiHei Pro', 'LiSong Pro', 'Libian SC','Libian TC', 'LingWai SC Medium','LingWai TC Medium', 'Lucida Grande','Lucida Grande Bold', 'Luminari', 'Maku','Maku Bold', 'Malayalam MN','Malayalam MN Bold','Malayalam Sangam MN','Malayalam Sangam MN Bold', 'Mali','Mali Bold','Mali Bold Italic','Mali ExtraLight','Mali ExtraLight Italic','Mali Italic','Mali Light','Mali Light Italic','Mali Medium','Mali Medium Italic','Mali SemiBold','Mali SemiBold Italic', 'Marker Felt Thin','Marker Felt Wide', 'Menlo','Menlo Bold','Menlo Bold Italic','Menlo Italic', 'Microsoft Sans Serif', 'Mishafi','Mishafi Gold', 'Modak', 'Monaco', 'Mshtakan','Mshtakan Bold','Mshtakan BoldOblique','Mshtakan Oblique', 'Mukta','Mukta Bold','Mukta ExtraBold','Mukta ExtraLight','Mukta Light','Mukta Malar','Mukta Malar Bold','Mukta Malar ExtraBold','Mukta Malar ExtraLight','Mukta Malar Light','Mukta Malar Medium','Mukta Malar SemiBold','Mukta Medium','Mukta SemiBold','Mukta Vaani','Mukta Vaani Bold','Mukta Vaani ExtraBold','Mukta Vaani ExtraLight','Mukta Vaani Light','Mukta Vaani Medium','Mukta Vaani SemiBold', 'Mukta Mahee','Mukta Mahee Bold','Mukta Mahee ExtraBold','Mukta Mahee ExtraLight','Mukta Mahee Light','Mukta Mahee Medium','Mukta Mahee SemiBold', 'Muna','Muna Black','Muna Bold', 'Myanmar MN','Myanmar MN Bold','Myanmar Sangam MN','Myanmar Sangam MN Bold', 'Myriad Arabic','Myriad Arabic Black','Myriad Arabic Black Italic','Myriad Arabic Bold','Myriad Arabic Bold Italic','Myriad Arabic Italic','Myriad Arabic Light','Myriad Arabic Light Italic','Myriad Arabic Semibold','Myriad Arabic Semibold Italic', 'Nadeem', 'Nanum Brush Script','Nanum Pen Script','Nanum Gothic','Nanum Gothic Bold','Nanum Gothic ExtraBold','Nanum Myeongjo','Nanum Myeongjo Bold','Nanum Myeongjo ExtraBold', 'New Peninim MT','New Peninim MT Bold','New Peninim MT Bold Inclined','New Peninim MT Inclined', 'Niramit','Niramit Bold','Niramit Bold Italic','Niramit ExtraLight','Niramit ExtraLight Italic','Niramit Italic','Niramit Light','Niramit Light Italic','Niramit Medium','Niramit Medium Italic','Niramit SemiBold','Niramit SemiBold Italic', 'Noteworthy Bold','Noteworthy Light', 'Noto Nastaliq Urdu','Noto Nastaliq Urdu Bold', 'Noto Sans Kannada','Noto Sans Kannada Black','Noto Sans Kannada Bold','Noto Sans Kannada ExtraBold','Noto Sans Kannada ExtraLight','Noto Sans Kannada Light','Noto Sans Kannada Medium','Noto Sans Kannada SemiBold','Noto Sans Kannada Thin','Noto Sans Myanmar','Noto Sans Myanmar Black','Noto Sans Myanmar Bold','Noto Sans Myanmar ExtraBold','Noto Sans Myanmar ExtraLight','Noto Sans Myanmar Light','Noto Sans Myanmar Medium','Noto Sans Myanmar SemiBold','Noto Sans Myanmar Thin','Noto Sans Oriya','Noto Sans Oriya Bold', 'Noto Serif Kannada','Noto Serif Kannada Black','Noto Serif Kannada Bold','Noto Serif Kannada ExtraBold','Noto Serif Kannada ExtraLight','Noto Serif Kannada Light','Noto Serif Kannada Medium','Noto Serif Kannada SemiBold','Noto Serif Kannada Thin','Noto Serif Myanmar','Noto Serif Myanmar Black','Noto Serif Myanmar Bold','Noto Serif Myanmar ExtraBold','Noto Serif Myanmar ExtraLight','Noto Serif Myanmar Light','Noto Serif Myanmar Medium','Noto Serif Myanmar SemiBold','Noto Serif Myanmar Thin', 'October Compressed Devanagari','October Compressed Devanagari Black','October Compressed Devanagari Bold','October Compressed Devanagari ExtraLight','October Compressed Devanagari Hairline','October Compressed Devanagari Heavy','October Compressed Devanagari Light','October Compressed Devanagari Medium','October Compressed Devanagari Thin','October Compressed Tamil','October Compressed Tamil Black','October Compressed Tamil Bold','October Compressed Tamil ExtraLight','October Compressed Tamil Hairline','October Compressed Tamil Heavy','October Compressed Tamil Light','October Compressed Tamil Medium','October Compressed Tamil Thin','October Condensed Devanagari','October Condensed Devanagari Black','October Condensed Devanagari Bold','October Condensed Devanagari ExtraLight','October Condensed Devanagari Hairline','October Condensed Devanagari Heavy','October Condensed Devanagari Light','October Condensed Devanagari Medium','October Condensed Devanagari Thin','October Condensed Tamil','October Condensed Tamil Black','October Condensed Tamil Bold','October Condensed Tamil ExtraLight','October Condensed Tamil Hairline','October Condensed Tamil Heavy','October Condensed Tamil Light','October Condensed Tamil Medium','October Condensed Tamil Thin','October Devanagari','October Devanagari Black','October Devanagari Bold','October Devanagari ExtraLight','October Devanagari Hairline','October Devanagari Heavy','October Devanagari Light','October Devanagari Medium','October Devanagari Thin','October Tamil','October Tamil Black','October Tamil Bold','October Tamil ExtraLight','October Tamil Hairline','October Tamil Heavy','October Tamil Light','October Tamil Medium','October Tamil Thin', 'Optima','Optima Bold','Optima Bold Italic','Optima ExtraBlack','Optima Italic', 'Oriya MN','Oriya MN Bold','Oriya Sangam MN','Oriya Sangam MN Bold', 'Osaka','Osaka-Mono', 'PCMyungjo', 'PSL Ornanong Pro','PSL Ornanong Pro Bold','PSL Ornanong Pro Bold Italic','PSL Ornanong Pro Demibold','PSL Ornanong Pro Demibold Italic','PSL Ornanong Pro Italic','PSL Ornanong Pro Light','PSL Ornanong Pro Light Italic', 'PT Mono','PT Mono Bold','PT Sans','PT Sans Bold','PT Sans Bold Italic','PT Sans Caption','PT Sans Caption Bold','PT Sans Italic','PT Sans Narrow','PT Sans Narrow Bold','PT Serif','PT Serif Bold','PT Serif Bold Italic','PT Serif Caption','PT Serif Caption Italic','PT Serif Italic', 'Padyakke Expanded One', 'Palatino','Palatino Bold','Palatino Bold Italic','Palatino Italic', 'Papyrus','Papyrus Condensed', 'Party LET', 'Phosphate Inline','Phosphate Solid', 'PilGi', 'PingFang HK','PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight','PingFang SC','PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight','PingFang TC','PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Plantagenet Cherokee', 'Produkt','Produkt Extralight','Produkt Extralight Italic','Produkt Light','Produkt Light Italic','Produkt Medium','Produkt Medium Italic','Produkt Italic', 'Proxima Nova','Proxima Nova Bold','Proxima Nova Bold It','Proxima Nova Extrabold','Proxima Nova Extrabold It','Proxima Nova It','Proxima Nova Light','Proxima Nova Light It','Proxima Nova Medium','Proxima Nova Medium It','Proxima Nova Semibold','Proxima Nova Semibold It', 'Publico Headline Black','Publico Headline Black Italic','Publico Headline Bold','Publico Headline Bold Italic','Publico Headline Italic','Publico Headline Roman','Publico Text Bold','Publico Text Bold Italic','Publico Text Italic','Publico Text Roman','Publico Text Semibold','Publico Text Semibold Italic', 'Quotes Caps','Quotes Script', 'Raanana','Raanana Bold', 'Rockwell','Rockwell Bold','Rockwell Bold Italic','Rockwell Italic', 'STFangsong', 'STHeiti', 'STIX Two Math','STIX Two Text','STIX Two Text Bold','STIX Two Text Bold Italic','STIX Two Text Italic','STIX Two Text Medium','STIX Two Text Medium Italic','STIX Two Text SemiBold','STIX Two Text SemiBold Italic', 'STKaiti', 'STSong', 'STXihei', 'Sama Devanagari','Sama Devanagari Bold','Sama Devanagari Book','Sama Devanagari ExtraBold','Sama Devanagari Medium','Sama Devanagari SemiBold','Sama Gujarati','Sama Gujarati Bold','Sama Gujarati Book','Sama Gujarati ExtraBold','Sama Gujarati Medium','Sama Gujarati SemiBold','Sama Gurmukhi','Sama Gurmukhi Bold','Sama Gurmukhi Book','Sama Gurmukhi ExtraBold','Sama Gurmukhi Medium','Sama Gurmukhi SemiBold','Sama Kannada','Sama Kannada Bold','Sama Kannada Book','Sama Kannada ExtraBold','Sama Kannada Medium','Sama Kannada SemiBold','Sama Malayalam','Sama Malayalam Bold','Sama Malayalam Book','Sama Malayalam ExtraBold','Sama Malayalam Medium','Sama Malayalam SemiBold','Sama Tamil','Sama Tamil Bold','Sama Tamil Book','Sama Tamil ExtraBold','Sama Tamil Medium','Sama Tamil SemiBold', 'Sana', 'Sarabun','Sarabun Bold','Sarabun Bold Italic','Sarabun ExtraBold','Sarabun ExtraBold Italic','Sarabun ExtraLight','Sarabun ExtraLight Italic','Sarabun Italic','Sarabun Light','Sarabun Light Italic','Sarabun Medium','Sarabun Medium Italic','Sarabun SemiBold','Sarabun SemiBold Italic','Sarabun Thin','Sarabun Thin Italic', 'Sathu', 'Sauber Script', 'Savoye LET', 'Shobhika','Shobhika Bold', 'Shree Devanagari 714','Shree Devanagari 714 Bold','Shree Devanagari 714 Bold Italic','Shree Devanagari 714 Italic', 'SignPainter','SignPainter Semibold', 'Silom', 'SimSong','SimSong Bold', 'Sinhala MN','Sinhala MN Bold','Sinhala Sangam MN','Sinhala Sangam MN Bold', 'Skia','Skia Black','Skia Black Condensed','Skia Black Extended','Skia Bold','Skia Condensed','Skia Extended','Skia Light','Skia Light Condensed','Skia Light Extended', 'Snell Roundhand','Snell Roundhand Black','Snell Roundhand Bold', 'Songti SC','Songti SC Black','Songti SC Bold','Songti SC Light','Songti TC','Songti TC Bold','Songti TC Light', 'Spot Mono','Spot Mono Bold','Spot Mono Medium', 'Srisakdi','Srisakdi Bold', 'Sukhumvit Set Bold','Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Semi Bold','Sukhumvit Set Text','Sukhumvit Set Thin', 'Symbol', 'Tahoma','Tahoma Bold', 'Tamil MN','Tamil MN Bold','Tamil Sangam MN','Tamil Sangam MN Black','Tamil Sangam MN Bold','Tamil Sangam MN Demibold','Tamil Sangam MN Light','Tamil Sangam MN Medium', 'Telugu MN','Telugu MN Bold','Telugu Sangam MN','Telugu Sangam MN Bold', 'Thonburi','Thonburi Bold','Thonburi Light', 'Times New Roman','Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Tiro Bangla','Tiro Bangla Italic','Tiro Devanagari Hindi','Tiro Devanagari Hindi Italic','Tiro Devanagari Marathi','Tiro Devanagari Marathi Italic','Tiro Devanagari Sanskrit','Tiro Devanagari Sanskrit Italic','Tiro Gurmukhi','Tiro Gurmukhi Italic','Tiro Kannada','Tiro Kannada Italic','Tiro Tamil','Tiro Tamil Italic','Tiro Telugu','Tiro Telugu Italic', 'Toppan Bunkyu Gothic','Toppan Bunkyu Gothic Demibold','Toppan Bunkyu Midashi Gothic Extrabold','Toppan Bunkyu Midashi Mincho Extrabold','Toppan Bunkyu Mincho', 'Trattatello', 'Trebuchet MS','Trebuchet MS Bold','Trebuchet MS Bold Italic','Trebuchet MS Italic', 'Tsukushi A Round Gothic','Tsukushi A Round Gothic Bold','Tsukushi B Round Gothic','Tsukushi B Round Gothic Bold', 'Verdana','Verdana Bold','Verdana Bold Italic','Verdana Italic', 'Waseem','Waseem Light', 'Wawati SC','Wawati TC', 'Webdings', 'Weibei SC Bold','Weibei TC Bold', 'Wingdings','Wingdings 2','Wingdings 3', 'Xingkai SC Bold','Xingkai SC Light','Xingkai TC Bold','Xingkai TC Light', 'YuGothic Bold','YuGothic Medium', 'YuKyokasho Bold','YuKyokasho Medium','YuKyokasho Yoko Bold','YuKyokasho Yoko Medium', 'YuMincho +36p Kana Demibold','YuMincho +36p Kana Extrabold','YuMincho +36p Kana Medium','YuMincho Demibold','YuMincho Extrabold','YuMincho Medium', 'Yuanti SC','Yuanti SC Bold','Yuanti SC Light','Yuanti TC','Yuanti TC Bold','Yuanti TC Light', 'Yuppy SC','Yuppy TC', 'Zapf Dingbats', 'Zapfino', ] }, 'v14': { name: 'Sonoma', version: 14, url: 'https://support.apple.com/en-us/108939', list: [ 'Academy Engraved LET', 'Adelle Sans Devanagari','Adelle Sans Devanagari Bold','Adelle Sans Devanagari Extrabold','Adelle Sans Devanagari Heavy','Adelle Sans Devanagari Light','Adelle Sans Devanagari Semibold','Adelle Sans Devanagari Thin', 'AkayaKanadaka','AkayaTelivigala', 'Al Bayan Bold','Al Bayan','Al Nile','Al Nile Bold','Al Tarikh', 'American Typewriter','American Typewriter Bold','American Typewriter Condensed','American Typewriter Condensed Bold','American Typewriter Condensed Light','American Typewriter Light','American Typewriter Semibold', 'Andale Mono', 'Annai MN', 'Apple Braille','Apple Braille Outline 6 Dot','Apple Braille Outline 8 Dot','Apple Braille Pinpoint 6 Dot','Apple Braille Pinpoint 8 Dot','Apple Chancery','Apple Color Emoji','Apple LiGothic Medium','Apple LiSung Light','Apple SD Gothic Neo','Apple SD Gothic Neo Bold','Apple SD Gothic Neo ExtraBold','Apple SD Gothic Neo Heavy','Apple SD Gothic Neo Light','Apple SD Gothic Neo Medium','Apple SD Gothic Neo SemiBold','Apple SD Gothic Neo Thin','Apple SD Gothic Neo UltraLight','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Black','Arial Bold','Arial Bold Italic','Arial Hebrew','Arial Hebrew Bold','Arial Hebrew Light','Arial Hebrew Scholar','Arial Hebrew Scholar Bold','Arial Hebrew Scholar Light','Arial Italic','Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic','Arial Rounded MT Bold','Arial Unicode MS', 'Arima Koshi','Arima Koshi Black','Arima Koshi Bold','Arima Koshi ExtraBold','Arima Koshi ExtraLight','Arima Koshi Light','Arima Koshi Medium','Arima Koshi Thin','Arima Madurai','Arima Madurai Black','Arima Madurai Bold','Arima Madurai ExtraLight','Arima Madurai Light','Arima Madurai Medium','Arima Madurai Semi Bold','Arima Madurai Thin', 'Avenir Black','Avenir Black Oblique','Avenir Book','Avenir Book Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light','Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Next','Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Condensed','Avenir Next Condensed Bold','Avenir Next Condensed Bold Italic','Avenir Next Condensed Demi Bold','Avenir Next Condensed Demi Bold Italic','Avenir Next Condensed Heavy','Avenir Next Condensed Heavy Italic','Avenir Next Condensed Italic','Avenir Next Condensed Medium','Avenir Next Condensed Medium Italic','Avenir Next Condensed Ultra Light','Avenir Next Condensed Ultra Light Italic','Avenir Next Demi Bold','Avenir Next Demi Bold Italic','Avenir Next Heavy','Avenir Next Heavy Italic','Avenir Next Italic','Avenir Next Medium','Avenir Next Medium Italic','Avenir Next Ultra Light','Avenir Next Ultra Light Italic','Avenir Oblique','Avenir Roman', 'Ayuthaya', 'BIZ UDGothic','BIZ UDGothic Bold','BIZ UDMincho', 'BM DoHyeon','BM Hanna 11yrs Old','BM Hanna Air','BM Hanna Pro','BM Jua','BM Kirang Haerang','BM Yeonsung', 'Baghdad', 'Bai Jamjuree','Bai Jamjuree Bold','Bai Jamjuree Bold Italic','Bai Jamjuree ExtraLight','Bai Jamjuree ExtraLight Italic','Bai Jamjuree Italic','Bai Jamjuree Light','Bai Jamjuree Light Italic','Bai Jamjuree Medium','Bai Jamjuree Medium Italic','Bai Jamjuree SemiBold','Bai Jamjuree SemiBold Italic', 'Baloo 2','Baloo 2 Bold','Baloo 2 ExtraBold','Baloo 2 Medium','Baloo 2 SemiBold','Baloo Bhai 2','Baloo Bhai 2 Bold','Baloo Bhai 2 ExtraBold','Baloo Bhai 2 Medium','Baloo Bhai 2 SemiBold','Baloo Bhaijaan','Baloo Bhaina 2','Baloo Bhaina 2 Bold','Baloo Bhaina 2 ExtraBold','Baloo Bhaina 2 Medium','Baloo Bhaina 2 SemiBold','Baloo Chettan 2','Baloo Chettan 2 Bold','Baloo Chettan 2 ExtraBold','Baloo Chettan 2 Medium','Baloo Chettan 2 SemiBold','Baloo Da 2','Baloo Da 2 Bold','Baloo Da 2 ExtraBold','Baloo Da 2 Medium','Baloo Da 2 SemiBold','Baloo Paaji 2','Baloo Paaji 2 Bold','Baloo Paaji 2 ExtraBold','Baloo Paaji 2 Medium','Baloo Paaji 2 SemiBold','Baloo Tamma 2','Baloo Tamma 2 Bold','Baloo Tamma 2 ExtraBold','Baloo Tamma 2 Medium','Baloo Tamma 2 SemiBold','Baloo Tammudu 2','Baloo Tammudu 2 Bold','Baloo Tammudu 2 ExtraBold','Baloo Tammudu 2 Medium','Baloo Tammudu 2 SemiBold','Baloo Thambi 2','Baloo Thambi 2 Bold','Baloo Thambi 2 ExtraBold','Baloo Thambi 2 Medium','Baloo Thambi 2 SemiBold', 'Bangla MN','Bangla MN Bold','Bangla Sangam MN','Bangla Sangam MN Bold', 'Baoli SC','Baoli TC', 'Baskerville','Baskerville Bold','Baskerville Bold Italic','Baskerville Italic','Baskerville SemiBold','Baskerville SemiBold Italic', 'Beirut', 'BiauKaiHK','BiauKaiTC', 'Big Caslon Medium', 'Bodoni 72 Bold','Bodoni 72 Book','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book','Bodoni 72 Oldstyle Book Italic','Bodoni 72 Smallcaps Book','Bodoni Ornaments', 'Bradley Hand Bold', 'Brill Bold','Brill Bold Italic','Brill Italic','Brill Roman', 'Brush Script MT Italic', 'Cambay Devanagari','Cambay Devanagari Bold','Cambay Devanagari Bold Oblique','Cambay Devanagari Oblique', 'Canela','Canela Bold','Canela Bold Italic','Canela Deck','Canela Deck Bold','Canela Deck Bold Italic','Canela Deck Medium','Canela Deck Medium Italic','Canela Deck Italic','Canela Italic','Canela Text','Canela Text Bold','Canela Text Bold Italic','Canela Text Medium','Canela Text Medium Italic','Canela Text Italic', 'Catamaran','Catamaran Black','Catamaran Bold','Catamaran ExtraBold','Catamaran ExtraLight','Catamaran Light','Catamaran Medium','Catamaran SemiBold','Catamaran Thin', 'Chakra Petch','Chakra Petch Bold','Chakra Petch Bold Italic','Chakra Petch ExtraLight','Chakra Petch ExtraLight Italic','Chakra Petch Italic','Chakra Petch Light','Chakra Petch Light Italic','Chakra Petch Medium','Chakra Petch Medium Italic','Chakra Petch SemiBold','Chakra Petch SemiBold Italic', 'Chalkboard','Chalkboard Bold','Chalkboard SE','Chalkboard SE Bold','Chalkboard SE Light','Chalkduster', 'Charm','Charm Bold', 'Charmonman','Charmonman Bold', 'Charter Black','Charter Black Italic','Charter Bold','Charter Bold Italic','Charter Italic','Charter Roman', 'Cochin','Cochin Bold','Cochin Bold Italic','Cochin Italic', 'Comic Sans MS','Comic Sans MS Bold', 'Copperplate','Copperplate Bold','Copperplate Light', 'Corsiva Hebrew','Corsiva Hebrew Bold', 'Courier New','Courier New Bold','Courier New Bold Italic','Courier New Italic', 'DIN Alternate Bold','DIN Condensed Bold', 'Damascus','Damascus Bold','Damascus Light','Damascus Medium','Damascus Semi Bold', 'DecoType Naskh', 'Devanagari MT','Devanagari MT Bold','Devanagari Sangam MN','Devanagari Sangam MN Bold', 'Didot','Didot Bold','Didot Italic', 'Diwan Kufi','Diwan Thuluth', 'Domaine Display','Domaine Display Bold','Domaine Display Bold Italic','Domaine Display Italic','Domaine Display Medium','Domaine Display Medium Italic', 'Euphemia UCAS','Euphemia UCAS Bold','Euphemia UCAS Italic', 'Fahkwang','Fahkwang Bold','Fahkwang Bold Italic','Fahkwang ExtraLight','Fahkwang ExtraLight Italic','Fahkwang Italic','Fahkwang Light','Fahkwang Light Italic','Fahkwang Medium','Fahkwang Medium Italic','Fahkwang SemiBold','Fahkwang SemiBold Italic', 'Farah', 'Farisi', 'Founders Grotesk','Founders Grotesk Bold','Founders Grotesk Bold Italic','Founders Grotesk Condensed','Founders Grotesk Condensed Bold','Founders Grotesk Condensed Semibold','Founders Grotesk Light','Founders Grotesk Light Italic','Founders Grotesk Medium','Founders Grotesk Medium Italic','Founders Grotesk Italic','Founders Grotesk Semibold','Founders Grotesk Semibold Italic','Founders Grotesk Text','Founders Grotesk Text Bold','Founders Grotesk Text Bold Italic','Founders Grotesk Text Italic', 'Futura Bold','Futura Condensed ExtraBold','Futura Condensed Medium','Futura Medium','Futura Medium Italic', 'GB18030 Bitmap', 'Galvji','Galvji Bold','Galvji Bold Oblique','Galvji Oblique', 'Geeza Pro','Geeza Pro Bold', 'Geneva', 'Georgia','Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Gill Sans','Gill Sans Bold','Gill Sans Bold Italic','Gill Sans Italic','Gill Sans Light','Gill Sans Light Italic','Gill Sans SemiBold','Gill Sans SemiBold Italic','Gill Sans UltraBold', 'Gotu', 'Grantha Sangam MN','Grantha Sangam MN Black','Grantha Sangam MN Bold','Grantha Sangam MN DemiBold','Grantha Sangam MN Light','Grantha Sangam MN Medium', 'Graphik','Graphik Bold','Graphik Bold Italic','Graphik Compact','Graphik Compact Bold','Graphik Compact Bold Italic','Graphik Compact Medium','Graphik Compact Medium Italic','Graphik Compact Italic','Graphik Compact Semibold','Graphik Compact Semibold Italic','Graphik Light','Graphik Light Italic','Graphik Medium','Graphik Medium Italic','Graphik Italic','Graphik Semibold','Graphik Semibold Italic', 'Gujarati MT','Gujarati MT Bold','Gujarati Sangam MN','Gujarati Sangam MN Bold', 'GungSeo', 'Gurmukhi MN','Gurmukhi MN Bold','Gurmukhi MT','Gurmukhi Sangam MN','Gurmukhi Sangam MN Bold', 'Hannotate SC','Hannotate SC Bold','Hannotate TC','Hannotate TC Bold', 'HanziPen SC','HanziPen SC Bold','HanziPen TC','HanziPen TC Bold', 'HeadLineA', 'Hei', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Helvetica','Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Neue','Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Condensed Black','Helvetica Neue Condensed Bold','Helvetica Neue Italic','Helvetica Neue Light','Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin','Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic','Helvetica Oblique', 'Herculanum', 'Hiragino Maru Gothic ProN W4','Hiragino Mincho ProN W3','Hiragino Mincho ProN W6','Hiragino Sans CNS W3','Hiragino Sans CNS W6','Hiragino Sans GB W3','Hiragino Sans GB W6','Hiragino Sans W0','Hiragino Sans W1','Hiragino Sans W2','Hiragino Sans W3','Hiragino Sans W4','Hiragino Sans W5','Hiragino Sans W6','Hiragino Sans W7','Hiragino Sans W8','Hiragino Sans W9', 'Hoefler Text','Hoefler Text Black','Hoefler Text Black Italic','Hoefler Text Italic','Hoefler Text Ornaments', 'Hubballi', 'ITF Devanagari Bold','ITF Devanagari Book','ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Marathi Bold','ITF Devanagari Marathi Book','ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium','ITF Devanagari Medium', 'Impact', 'InaiMathi','InaiMathi Bold', 'Jaini','Jaini Purva', 'K2D','K2D Bold','K2D Bold Italic','K2D ExtraBold','K2D ExtraBold Italic','K2D ExtraLight','K2D ExtraLight Italic','K2D Italic','K2D Light','K2D Light Italic','K2D Medium','K2D Medium Italic','K2D SemiBold','K2D SemiBold Italic','K2D Thin','K2D Thin Italic', 'Kai', 'Kailasa','Kailasa Bold', 'Kaiti SC','Kaiti SC Black','Kaiti SC Bold','Kaiti TC','Kaiti TC Black','Kaiti TC Bold', 'Kannada MN','Kannada MN Bold','Kannada Sangam MN','Kannada Sangam MN Bold', 'Katari','Katari Black','Katari Black Italic','Katari Bold','Katari Bold Italic','Katari Italic','Katari Medium','Katari Medium Italic', 'Kavivanar', 'Kefa','Kefa Bold', 'Khmer MN','Khmer MN Bold','Khmer Sangam MN', 'Kigelia','Kigelia Arabic','Kigelia Arabic Bold','Kigelia Arabic Extrabold','Kigelia Arabic Light','Kigelia Arabic Semibold','Kigelia Bold','Kigelia Bold Italic','Kigelia Extrabold','Kigelia Extrabold Italic','Kigelia Italic','Kigelia Light','Kigelia Light Italic','Kigelia Semibold','Kigelia Semibold Italic', 'Klee Demibold','Klee Medium', 'KoHo','KoHo Bold','KoHo Bold Italic','KoHo ExtraLight','KoHo ExtraLight Italic','KoHo Italic','KoHo Light','KoHo Light Italic','KoHo Medium','KoHo Medium Italic','KoHo SemiBold','KoHo SemiBold Italic', 'Kodchasan','Kodchasan Bold','Kodchasan Bold Italic','Kodchasan ExtraLight','Kodchasan ExtraLight Italic','Kodchasan Italic','Kodchasan Light','Kodchasan Light Italic','Kodchasan Medium','Kodchasan Medium Italic','Kodchasan SemiBold','Kodchasan SemiBold Italic', 'Kohinoor Bangla','Kohinoor Bangla Bold','Kohinoor Bangla Light','Kohinoor Bangla Medium','Kohinoor Bangla Semibold','Kohinoor Devanagari','Kohinoor Devanagari Bold','Kohinoor Devanagari Light','Kohinoor Devanagari Medium','Kohinoor Devanagari Semibold','Kohinoor Gujarati','Kohinoor Gujarati Bold','Kohinoor Gujarati Light','Kohinoor Gujarati Medium','Kohinoor Gujarati Semibold','Kohinoor Telugu','Kohinoor Telugu Bold','Kohinoor Telugu Light','Kohinoor Telugu Medium','Kohinoor Telugu Semibold', 'Kokonor', 'Krub','Krub Bold','Krub Bold Italic','Krub ExtraLight','Krub ExtraLight Italic','Krub Italic','Krub Light','Krub Light Italic','Krub Medium','Krub Medium Italic','Krub SemiBold','Krub SemiBold Italic', 'Krungthep', 'KufiStandardGK', 'Lahore Gurmukhi','Lahore Gurmukhi Bold','Lahore Gurmukhi Light','Lahore Gurmukhi Medium','Lahore Gurmukhi SemiBold', 'Lantinghei SC Demibold','Lantinghei SC Extralight','Lantinghei SC Heavy','Lantinghei TC Demibold','Lantinghei TC Heavy', 'Lao MN','Lao MN Bold','Lao Sangam MN', 'Lava Devanagari','Lava Devanagari Bold','Lava Devanagari Heavy','Lava Devanagari Medium','Lava Kannada','Lava Kannada Bold','Lava Kannada Heavy','Lava Kannada Medium','Lava Telugu','Lava Telugu Bold','Lava Telugu Heavy','Lava Telugu Medium', 'LiHei Pro', 'LiSong Pro', 'Libian SC','Libian TC', 'LingWai SC Medium','LingWai TC Medium', 'Lucida Grande','Lucida Grande Bold', 'Luminari', 'Maku','Maku Bold', 'Malayalam MN','Malayalam MN Bold','Malayalam Sangam MN','Malayalam Sangam MN Bold', 'Mali','Mali Bold','Mali Bold Italic','Mali ExtraLight','Mali ExtraLight Italic','Mali Italic','Mali Light','Mali Light Italic','Mali Medium','Mali Medium Italic','Mali SemiBold','Mali SemiBold Italic', 'Marker Felt Thin','Marker Felt Wide', 'Menlo','Menlo Bold','Menlo Bold Italic','Menlo Italic', 'Microsoft Sans Serif', 'Mishafi','Mishafi Gold', 'Modak', 'Monaco', 'Mshtakan','Mshtakan Bold','Mshtakan BoldOblique','Mshtakan Oblique', 'Mukta','Mukta Bold','Mukta ExtraBold','Mukta ExtraLight','Mukta Light','Mukta Malar','Mukta Malar Bold','Mukta Malar ExtraBold','Mukta Malar ExtraLight','Mukta Malar Light','Mukta Malar Medium','Mukta Malar SemiBold','Mukta Medium','Mukta SemiBold','Mukta Vaani','Mukta Vaani Bold','Mukta Vaani ExtraBold','Mukta Vaani ExtraLight','Mukta Vaani Light','Mukta Vaani Medium','Mukta Vaani SemiBold', 'Mukta Mahee','Mukta Mahee Bold','Mukta Mahee ExtraBold','Mukta Mahee ExtraLight','Mukta Mahee Light','Mukta Mahee Medium','Mukta Mahee SemiBold', 'Muna','Muna Black','Muna Bold', 'Myanmar MN','Myanmar MN Bold','Myanmar Sangam MN','Myanmar Sangam MN Bold', 'Myriad Arabic','Myriad Arabic Black','Myriad Arabic Black Italic','Myriad Arabic Bold','Myriad Arabic Bold Italic','Myriad Arabic Italic','Myriad Arabic Light','Myriad Arabic Light Italic','Myriad Arabic Semibold','Myriad Arabic Semibold Italic', 'Nadeem', 'Nanum Brush Script','Nanum Pen Script','Nanum Gothic','Nanum Gothic Bold','Nanum Gothic ExtraBold','Nanum Myeongjo','Nanum Myeongjo Bold','Nanum Myeongjo ExtraBold', 'New Peninim MT','New Peninim MT Bold','New Peninim MT Bold Inclined','New Peninim MT Inclined', 'Niramit','Niramit Bold','Niramit Bold Italic','Niramit ExtraLight','Niramit ExtraLight Italic','Niramit Italic','Niramit Light','Niramit Light Italic','Niramit Medium','Niramit Medium Italic','Niramit SemiBold','Niramit SemiBold Italic', 'Noteworthy Bold','Noteworthy Light', 'Noto Nastaliq Urdu','Noto Nastaliq Urdu Bold', 'Noto Sans Batak','Noto Sans Kannada','Noto Sans Kannada Black','Noto Sans Kannada Bold','Noto Sans Kannada ExtraBold','Noto Sans Kannada ExtraLight','Noto Sans Kannada Light','Noto Sans Kannada Medium','Noto Sans Kannada SemiBold','Noto Sans Kannada Thin','Noto Sans Myanmar','Noto Sans Myanmar Black','Noto Sans Myanmar Bold','Noto Sans Myanmar ExtraBold','Noto Sans Myanmar ExtraLight','Noto Sans Myanmar Light','Noto Sans Myanmar Medium','Noto Sans Myanmar SemiBold','Noto Sans Myanmar Thin','Noto Sans NKo','Noto Sans Oriya','Noto Sans Oriya Bold','Noto Sans Tagalog', 'Noto Serif Kannada','Noto Serif Kannada Black','Noto Serif Kannada Bold','Noto Serif Kannada ExtraBold','Noto Serif Kannada ExtraLight','Noto Serif Kannada Light','Noto Serif Kannada Medium','Noto Serif Kannada SemiBold','Noto Serif Kannada Thin','Noto Serif Myanmar','Noto Serif Myanmar Black','Noto Serif Myanmar Bold','Noto Serif Myanmar ExtraBold','Noto Serif Myanmar ExtraLight','Noto Serif Myanmar Light','Noto Serif Myanmar Medium','Noto Serif Myanmar SemiBold','Noto Serif Myanmar Thin', 'October Compressed Devanagari','October Compressed Devanagari Black','October Compressed Devanagari Bold','October Compressed Devanagari ExtraLight','October Compressed Devanagari Hairline','October Compressed Devanagari Heavy','October Compressed Devanagari Light','October Compressed Devanagari Medium','October Compressed Devanagari Thin','October Compressed Gujarati','October Compressed Gujarati Black','October Compressed Gujarati Bold','October Compressed Gujarati ExtraLight','October Compressed Gujarati Hairline','October Compressed Gujarati Heavy','October Compressed Gujarati Light','October Compressed Gujarati Medium','October Compressed Gujarati Thin','October Compressed Gurmukhi','October Compressed Gurmukhi Black','October Compressed Gurmukhi Bold','October Compressed Gurmukhi ExtraLight','October Compressed Gurmukhi Hairline','October Compressed Gurmukhi Heavy','October Compressed Gurmukhi Light','October Compressed Gurmukhi Medium','October Compressed Gurmukhi Thin','October Compressed Kannada','October Compressed Kannada Black','October Compressed Kannada Bold','October Compressed Kannada ExtraLight','October Compressed Kannada Hairline','October Compressed Kannada Heavy','October Compressed Kannada Light','October Compressed Kannada Medium','October Compressed Kannada Thin','October Compressed Meetei Mayek','October Compressed Meetei Mayek Black','October Compressed Meetei Mayek Bold','October Compressed Meetei Mayek ExtraLight','October Compressed Meetei Mayek Hairline','October Compressed Meetei Mayek Heavy','October Compressed Meetei Mayek Light','October Compressed Meetei Mayek Medium','October Compressed Meetei Mayek Thin','October Compressed Odia','October Compressed Odia Black','October Compressed Odia Bold','October Compressed Odia ExtraLight','October Compressed Odia Hairline','October Compressed Odia Heavy','October Compressed Odia Light','October Compressed Odia Medium','October Compressed Odia Thin','October Compressed Ol Chiki','October Compressed Ol Chiki Black','October Compressed Ol Chiki Bold','October Compressed Ol Chiki ExtraLight','October Compressed Ol Chiki Hairline','October Compressed Ol Chiki Heavy','October Compressed Ol Chiki Light','October Compressed Ol Chiki Medium','October Compressed Ol Chiki Thin','October Compressed Tamil','October Compressed Tamil Black','October Compressed Tamil Bold','October Compressed Tamil ExtraLight','October Compressed Tamil Hairline','October Compressed Tamil Heavy','October Compressed Tamil Light','October Compressed Tamil Medium','October Compressed Tamil Thin','October Compressed Telugu','October Compressed Telugu Black','October Compressed Telugu Bold','October Compressed Telugu ExtraLight','October Compressed Telugu Hairline','October Compressed Telugu Heavy','October Compressed Telugu Light','October Compressed Telugu Medium','October Compressed Telugu Thin','October Condensed Devanagari','October Condensed Devanagari Black','October Condensed Devanagari Bold','October Condensed Devanagari ExtraLight','October Condensed Devanagari Hairline','October Condensed Devanagari Heavy','October Condensed Devanagari Light','October Condensed Devanagari Medium','October Condensed Devanagari Thin','October Condensed Gujarati','October Condensed Gujarati Black','October Condensed Gujarati Bold','October Condensed Gujarati ExtraLight','October Condensed Gujarati Hairline','October Condensed Gujarati Heavy','October Condensed Gujarati Light','October Condensed Gujarati Medium','October Condensed Gujarati Thin','October Condensed Gurmukhi','October Condensed Gurmukhi Black','October Condensed Gurmukhi Bold','October Condensed Gurmukhi ExtraLight','October Condensed Gurmukhi Hairline','October Condensed Gurmukhi Heavy','October Condensed Gurmukhi Light','October Condensed Gurmukhi Medium','October Condensed Gurmukhi Thin','October Condensed Kannada','October Condensed Kannada Black','October Condensed Kannada Bold','October Condensed Kannada ExtraLight','October Condensed Kannada Hairline','October Condensed Kannada Heavy','October Condensed Kannada Light','October Condensed Kannada Medium','October Condensed Kannada Thin','October Condensed Meetei Mayek','October Condensed Meetei Mayek Black','October Condensed Meetei Mayek Bold','October Condensed Meetei Mayek ExtraLight','October Condensed Meetei Mayek Hairline','October Condensed Meetei Mayek Heavy','October Condensed Meetei Mayek Light','October Condensed Meetei Mayek Medium','October Condensed Meetei Mayek Thin','October Condensed Odia','October Condensed Odia Black','October Condensed Odia Bold','October Condensed Odia ExtraLight','October Condensed Odia Hairline','October Condensed Odia Heavy','October Condensed Odia Light','October Condensed Odia Medium','October Condensed Odia Thin','October Condensed Ol Chiki','October Condensed Ol Chiki Black','October Condensed Ol Chiki Bold','October Condensed Ol Chiki ExtraLight','October Condensed Ol Chiki Hairline','October Condensed Ol Chiki Heavy','October Condensed Ol Chiki Light','October Condensed Ol Chiki Medium','October Condensed Ol Chiki Thin','October Condensed Tamil','October Condensed Tamil Black','October Condensed Tamil Bold','October Condensed Tamil ExtraLight','October Condensed Tamil Hairline','October Condensed Tamil Heavy','October Condensed Tamil Light','October Condensed Tamil Medium','October Condensed Tamil Thin','October Condensed Telugu','October Condensed Telugu Black','October Condensed Telugu Bold','October Condensed Telugu ExtraLight','October Condensed Telugu Hairline','October Condensed Telugu Heavy','October Condensed Telugu Light','October Condensed Telugu Medium','October Condensed Telugu Thin','October Devanagari','October Devanagari Black','October Devanagari Bold','October Devanagari ExtraLight','October Devanagari Hairline','October Devanagari Heavy','October Devanagari Light','October Devanagari Medium','October Devanagari Thin','October Gujarati','October Gujarati Black','October Gujarati Bold','October Gujarati ExtraLight','October Gujarati Hairline','October Gujarati Heavy','October Gujarati Light','October Gujarati Medium','October Gujarati Thin','October Gurmukhi','October Gurmukhi Black','October Gurmukhi Bold','October Gurmukhi ExtraLight','October Gurmukhi Hairline','October Gurmukhi Heavy','October Gurmukhi Light','October Gurmukhi Medium','October Gurmukhi Thin','October Kannada','October Kannada Black','October Kannada Bold','October Kannada ExtraLight','October Kannada Hairline','October Kannada Heavy','October Kannada Light','October Kannada Medium','October Kannada Thin','October Meetei Mayek','October Meetei Mayek Black','October Meetei Mayek Bold','October Meetei Mayek ExtraLight','October Meetei Mayek Hairline','October Meetei Mayek Heavy','October Meetei Mayek Light','October Meetei Mayek Medium','October Meetei Mayek Thin','October Odia','October Odia Black','October Odia Bold','October Odia ExtraLight','October Odia Hairline','October Odia Heavy','October Odia Light','October Odia Medium','October Odia Thin','October Ol Chiki','October Ol Chiki Black','October Ol Chiki Bold','October Ol Chiki ExtraLight','October Ol Chiki Hairline','October Ol Chiki Heavy','October Ol Chiki Light','October Ol Chiki Medium','October Ol Chiki Thin','October Tamil','October Tamil Black','October Tamil Bold','October Tamil ExtraLight','October Tamil Hairline','October Tamil Heavy','October Tamil Light','October Tamil Medium','October Tamil Thin','October Telugu','October Telugu Black','October Telugu Bold','October Telugu ExtraLight','October Telugu Hairline','October Telugu Heavy','October Telugu Light','October Telugu Medium','October Telugu Thin', 'Optima','Optima Bold','Optima Bold Italic','Optima ExtraBlack','Optima Italic', 'Oriya MN','Oriya MN Bold','Oriya Sangam MN','Oriya Sangam MN Bold', 'Osaka','Osaka-Mono', 'PCMyungjo', 'PSL Ornanong Pro','PSL Ornanong Pro Bold','PSL Ornanong Pro Bold Italic','PSL Ornanong Pro Demibold','PSL Ornanong Pro Demibold Italic','PSL Ornanong Pro Italic','PSL Ornanong Pro Light','PSL Ornanong Pro Light Italic', 'PT Mono','PT Mono Bold','PT Sans','PT Sans Bold','PT Sans Bold Italic','PT Sans Caption','PT Sans Caption Bold','PT Sans Italic','PT Sans Narrow','PT Sans Narrow Bold','PT Serif','PT Serif Bold','PT Serif Bold Italic','PT Serif Caption','PT Serif Caption Italic','PT Serif Italic', 'Padyakke Expanded One', 'Palatino','Palatino Bold','Palatino Bold Italic','Palatino Italic', 'Papyrus','Papyrus Condensed', 'Party LET', 'Phosphate Inline','Phosphate Solid', 'PilGi', 'PingFang HK','PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight','PingFang SC','PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight','PingFang TC','PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Plantagenet Cherokee', 'Produkt','Produkt Extralight','Produkt Extralight Italic','Produkt Light','Produkt Light Italic','Produkt Medium','Produkt Medium Italic','Produkt Italic', 'Proxima Nova','Proxima Nova Bold','Proxima Nova Bold It','Proxima Nova Extrabold','Proxima Nova Extrabold It','Proxima Nova It','Proxima Nova Light','Proxima Nova Light It','Proxima Nova Medium','Proxima Nova Medium It','Proxima Nova Semibold','Proxima Nova Semibold It', 'Publico Headline Black','Publico Headline Black Italic','Publico Headline Bold','Publico Headline Bold Italic','Publico Headline Italic','Publico Headline Roman','Publico Text Bold','Publico Text Bold Italic','Publico Text Italic','Publico Text Roman','Publico Text Semibold','Publico Text Semibold Italic', 'Quotes Caps','Quotes Script', 'Raanana','Raanana Bold', 'Rockwell','Rockwell Bold','Rockwell Bold Italic','Rockwell Italic', 'STFangsong', 'STHeiti', 'STIX Two Math','STIX Two Text','STIX Two Text Bold','STIX Two Text Bold Italic','STIX Two Text Italic','STIX Two Text Medium','STIX Two Text Medium Italic','STIX Two Text SemiBold','STIX Two Text SemiBold Italic', 'STKaiti', 'STSong', 'STXihei', 'Sama Devanagari','Sama Devanagari Bold','Sama Devanagari Book','Sama Devanagari ExtraBold','Sama Devanagari Medium','Sama Devanagari SemiBold','Sama Gujarati','Sama Gujarati Bold','Sama Gujarati Book','Sama Gujarati ExtraBold','Sama Gujarati Medium','Sama Gujarati SemiBold','Sama Gurmukhi','Sama Gurmukhi Bold','Sama Gurmukhi Book','Sama Gurmukhi ExtraBold','Sama Gurmukhi Medium','Sama Gurmukhi SemiBold','Sama Kannada','Sama Kannada Bold','Sama Kannada Book','Sama Kannada ExtraBold','Sama Kannada Medium','Sama Kannada SemiBold','Sama Malayalam','Sama Malayalam Bold','Sama Malayalam Book','Sama Malayalam ExtraBold','Sama Malayalam Medium','Sama Malayalam SemiBold','Sama Tamil','Sama Tamil Bold','Sama Tamil Book','Sama Tamil ExtraBold','Sama Tamil Medium','Sama Tamil SemiBold', 'Sana', 'Sarabun','Sarabun Bold','Sarabun Bold Italic','Sarabun ExtraBold','Sarabun ExtraBold Italic','Sarabun ExtraLight','Sarabun ExtraLight Italic','Sarabun Italic','Sarabun Light','Sarabun Light Italic','Sarabun Medium','Sarabun Medium Italic','Sarabun SemiBold','Sarabun SemiBold Italic','Sarabun Thin','Sarabun Thin Italic', 'Sathu', 'Sauber Script', 'Savoye LET', 'Shobhika','Shobhika Bold', 'Shree Devanagari 714','Shree Devanagari 714 Bold','Shree Devanagari 714 Bold Italic','Shree Devanagari 714 Italic', 'SignPainter','SignPainter Semibold', 'Silom', 'SimSong','SimSong Bold', 'Sinhala MN','Sinhala MN Bold','Sinhala Sangam MN','Sinhala Sangam MN Bold', 'Skia','Skia Black','Skia Black Condensed','Skia Black Extended','Skia Bold','Skia Condensed','Skia Extended','Skia Light','Skia Light Condensed','Skia Light Extended', 'Snell Roundhand','Snell Roundhand Black','Snell Roundhand Bold', 'Songti SC','Songti SC Black','Songti SC Bold','Songti SC Light','Songti TC','Songti TC Bold','Songti TC Light', 'Spot Mono','Spot Mono Bold','Spot Mono Medium', 'Srisakdi','Srisakdi Bold', 'Sukhumvit Set Bold','Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Semi Bold','Sukhumvit Set Text','Sukhumvit Set Thin', 'Symbol', 'Tahoma','Tahoma Bold', 'Tamil MN','Tamil MN Bold','Tamil Sangam MN','Tamil Sangam MN Black','Tamil Sangam MN Bold','Tamil Sangam MN Demibold','Tamil Sangam MN Light','Tamil Sangam MN Medium', 'Telugu MN','Telugu MN Bold','Telugu Sangam MN','Telugu Sangam MN Bold', 'Thonburi','Thonburi Bold','Thonburi Light', 'Times New Roman','Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Tiro Bangla','Tiro Bangla Italic','Tiro Devanagari Hindi','Tiro Devanagari Hindi Italic','Tiro Devanagari Marathi','Tiro Devanagari Marathi Italic','Tiro Devanagari Sanskrit','Tiro Devanagari Sanskrit Italic','Tiro Gurmukhi','Tiro Gurmukhi Italic','Tiro Kannada','Tiro Kannada Italic','Tiro Tamil','Tiro Tamil Italic','Tiro Telugu','Tiro Telugu Italic', 'Toppan Bunkyu Gothic','Toppan Bunkyu Gothic Demibold','Toppan Bunkyu Midashi Gothic Extrabold','Toppan Bunkyu Midashi Mincho Extrabold','Toppan Bunkyu Mincho', 'Trattatello', 'Trebuchet MS','Trebuchet MS Bold','Trebuchet MS Bold Italic','Trebuchet MS Italic', 'Tsukushi A Round Gothic','Tsukushi A Round Gothic Bold','Tsukushi B Round Gothic','Tsukushi B Round Gothic Bold', 'Verdana','Verdana Bold','Verdana Bold Italic','Verdana Italic', 'Waseem','Waseem Light', 'Wawati SC','Wawati TC', 'Webdings', 'Weibei SC Bold','Weibei TC Bold', 'Wingdings','Wingdings 2','Wingdings 3', 'Xingkai SC Bold','Xingkai SC Light','Xingkai TC Bold','Xingkai TC Light', 'YuGothic Bold','YuGothic Medium', 'YuKyokasho Bold','YuKyokasho Medium','YuKyokasho Yoko Bold','YuKyokasho Yoko Medium', 'YuMincho +36p Kana Demibold','YuMincho +36p Kana Extrabold','YuMincho +36p Kana Medium','YuMincho Demibold','YuMincho Extrabold','YuMincho Medium', 'Yuanti SC','Yuanti SC Bold','Yuanti SC Light','Yuanti TC','Yuanti TC Bold','Yuanti TC Light', 'Yuppy SC','Yuppy TC', 'Zapf Dingbats', 'Zapfino', ] }, 'v15': { name: 'Sequoia', version: 15, url: 'https://support.apple.com/en-us/120414', list: [ 'Academy Engraved LET', 'Adelle Sans Devanagari','Adelle Sans Devanagari Bold','Adelle Sans Devanagari Extrabold','Adelle Sans Devanagari Heavy','Adelle Sans Devanagari Light','Adelle Sans Devanagari Semibold','Adelle Sans Devanagari Thin', 'AkayaKanadaka','AkayaTelivigala', 'Al Bayan Bold','Al Bayan','Al Nile','Al Nile Bold','Al Tarikh', 'American Typewriter','American Typewriter Bold','American Typewriter Condensed','American Typewriter Condensed Bold','American Typewriter Condensed Light','American Typewriter Light','American Typewriter Semibold', 'Andale Mono', 'Annai MN', 'Apple Braille','Apple Braille Outline 6 Dot','Apple Braille Outline 8 Dot','Apple Braille Pinpoint 6 Dot','Apple Braille Pinpoint 8 Dot','Apple Chancery','Apple Color Emoji','Apple LiGothic Medium','Apple LiSung Light','Apple SD Gothic Neo','Apple SD Gothic Neo Bold','Apple SD Gothic Neo ExtraBold','Apple SD Gothic Neo Heavy','Apple SD Gothic Neo Light','Apple SD Gothic Neo Medium','Apple SD Gothic Neo SemiBold','Apple SD Gothic Neo Thin','Apple SD Gothic Neo UltraLight','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Black','Arial Bold','Arial Bold Italic','Arial Hebrew','Arial Hebrew Bold','Arial Hebrew Light','Arial Hebrew Scholar','Arial Hebrew Scholar Bold','Arial Hebrew Scholar Light','Arial Italic','Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic','Arial Rounded MT Bold','Arial Unicode MS', 'Arima Koshi','Arima Koshi Black','Arima Koshi Bold','Arima Koshi ExtraBold','Arima Koshi ExtraLight','Arima Koshi Light','Arima Koshi Medium','Arima Koshi Thin','Arima Madurai','Arima Madurai Black','Arima Madurai Bold','Arima Madurai ExtraLight','Arima Madurai Light','Arima Madurai Medium','Arima Madurai Semi Bold','Arima Madurai Thin', 'Avenir Black','Avenir Black Oblique','Avenir Book','Avenir Book Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light','Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Next','Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Condensed','Avenir Next Condensed Bold','Avenir Next Condensed Bold Italic','Avenir Next Condensed Demi Bold','Avenir Next Condensed Demi Bold Italic','Avenir Next Condensed Heavy','Avenir Next Condensed Heavy Italic','Avenir Next Condensed Italic','Avenir Next Condensed Medium','Avenir Next Condensed Medium Italic','Avenir Next Condensed Ultra Light','Avenir Next Condensed Ultra Light Italic','Avenir Next Demi Bold','Avenir Next Demi Bold Italic','Avenir Next Heavy','Avenir Next Heavy Italic','Avenir Next Italic','Avenir Next Medium','Avenir Next Medium Italic','Avenir Next Ultra Light','Avenir Next Ultra Light Italic','Avenir Oblique','Avenir Roman', 'Ayuthaya', 'BIZ UDGothic','BIZ UDGothic Bold','BIZ UDMincho', 'BM DoHyeon','BM Hanna 11yrs Old','BM Hanna Air','BM Hanna Pro','BM Jua','BM Kirang Haerang','BM Yeonsung', 'Baghdad', 'Bai Jamjuree','Bai Jamjuree Bold','Bai Jamjuree Bold Italic','Bai Jamjuree ExtraLight','Bai Jamjuree ExtraLight Italic','Bai Jamjuree Italic','Bai Jamjuree Light','Bai Jamjuree Light Italic','Bai Jamjuree Medium','Bai Jamjuree Medium Italic','Bai Jamjuree SemiBold','Bai Jamjuree SemiBold Italic', 'Baloo 2','Baloo 2 Bold','Baloo 2 ExtraBold','Baloo 2 Medium','Baloo 2 SemiBold','Baloo Bhai 2','Baloo Bhai 2 Bold','Baloo Bhai 2 ExtraBold','Baloo Bhai 2 Medium','Baloo Bhai 2 SemiBold','Baloo Bhaijaan','Baloo Bhaina 2','Baloo Bhaina 2 Bold','Baloo Bhaina 2 ExtraBold','Baloo Bhaina 2 Medium','Baloo Bhaina 2 SemiBold','Baloo Chettan 2','Baloo Chettan 2 Bold','Baloo Chettan 2 ExtraBold','Baloo Chettan 2 Medium','Baloo Chettan 2 SemiBold','Baloo Da 2','Baloo Da 2 Bold','Baloo Da 2 ExtraBold','Baloo Da 2 Medium','Baloo Da 2 SemiBold','Baloo Paaji 2','Baloo Paaji 2 Bold','Baloo Paaji 2 ExtraBold','Baloo Paaji 2 Medium','Baloo Paaji 2 SemiBold','Baloo Tamma 2','Baloo Tamma 2 Bold','Baloo Tamma 2 ExtraBold','Baloo Tamma 2 Medium','Baloo Tamma 2 SemiBold','Baloo Tammudu 2','Baloo Tammudu 2 Bold','Baloo Tammudu 2 ExtraBold','Baloo Tammudu 2 Medium','Baloo Tammudu 2 SemiBold','Baloo Thambi 2','Baloo Thambi 2 Bold','Baloo Thambi 2 ExtraBold','Baloo Thambi 2 Medium','Baloo Thambi 2 SemiBold', 'Bangla MN','Bangla MN Bold','Bangla Sangam MN','Bangla Sangam MN Bold', 'Baoli SC','Baoli TC', 'Baskerville','Baskerville Bold','Baskerville Bold Italic','Baskerville Italic','Baskerville SemiBold','Baskerville SemiBold Italic', 'Beirut', 'BiauKaiHK','BiauKaiTC', 'Big Caslon Medium', 'Bodoni 72 Bold','Bodoni 72 Book','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book','Bodoni 72 Oldstyle Book Italic','Bodoni 72 Smallcaps Book','Bodoni Ornaments', 'Bradley Hand Bold', 'Brill Bold Italic','Brill Italic','Brill Medium Italic','Brill Roman','Brill Roman Bold','Brill Roman Medium','Brill Roman Semibold','Brill Semibold Italic', 'Brush Script MT Italic', 'Cambay Devanagari','Cambay Devanagari Bold','Cambay Devanagari Bold Oblique','Cambay Devanagari Oblique', 'Canela','Canela Bold','Canela Bold Italic','Canela Deck','Canela Deck Bold','Canela Deck Bold Italic','Canela Deck Medium','Canela Deck Medium Italic','Canela Deck Italic','Canela Italic','Canela Text','Canela Text Bold','Canela Text Bold Italic','Canela Text Medium','Canela Text Medium Italic','Canela Text Italic', 'Catamaran','Catamaran Black','Catamaran Bold','Catamaran ExtraBold','Catamaran ExtraLight','Catamaran Light','Catamaran Medium','Catamaran SemiBold','Catamaran Thin', 'Chakra Petch','Chakra Petch Bold','Chakra Petch Bold Italic','Chakra Petch ExtraLight','Chakra Petch ExtraLight Italic','Chakra Petch Italic','Chakra Petch Light','Chakra Petch Light Italic','Chakra Petch Medium','Chakra Petch Medium Italic','Chakra Petch SemiBold','Chakra Petch SemiBold Italic', 'Chalkboard','Chalkboard Bold','Chalkboard SE','Chalkboard SE Bold','Chalkboard SE Light','Chalkduster', 'Charm','Charm Bold', 'Charmonman','Charmonman Bold', 'Charter Black','Charter Black Italic','Charter Bold','Charter Bold Italic','Charter Italic','Charter Roman', 'Cochin','Cochin Bold','Cochin Bold Italic','Cochin Italic', 'Comic Sans MS','Comic Sans MS Bold', 'Copperplate','Copperplate Bold','Copperplate Light', 'Corsiva Hebrew','Corsiva Hebrew Bold', 'Courier New','Courier New Bold','Courier New Bold Italic','Courier New Italic', 'DFKaiShu-SB-Estd-BF', 'DIN Alternate Bold','DIN Condensed Bold', 'Damascus','Damascus Bold','Damascus Light','Damascus Medium','Damascus Semi Bold', 'Dash','Dash Practice', 'DecoType Naskh', 'Devanagari MT','Devanagari MT Bold','Devanagari Sangam MN','Devanagari Sangam MN Bold', 'Didot','Didot Bold','Didot Italic', 'Diwan Kufi','Diwan Thuluth', 'Domaine Display','Domaine Display Bold','Domaine Display Bold Italic','Domaine Display Italic','Domaine Display Medium','Domaine Display Medium Italic', 'Euphemia UCAS','Euphemia UCAS Bold','Euphemia UCAS Italic', 'Fahkwang','Fahkwang Bold','Fahkwang Bold Italic','Fahkwang ExtraLight','Fahkwang ExtraLight Italic','Fahkwang Italic','Fahkwang Light','Fahkwang Light Italic','Fahkwang Medium','Fahkwang Medium Italic','Fahkwang SemiBold','Fahkwang SemiBold Italic', 'Farah', 'Farisi', 'Founders Grotesk','Founders Grotesk Bold','Founders Grotesk Bold Italic','Founders Grotesk Condensed','Founders Grotesk Condensed Bold','Founders Grotesk Condensed Semibold','Founders Grotesk Light','Founders Grotesk Light Italic','Founders Grotesk Medium','Founders Grotesk Medium Italic','Founders Grotesk Italic','Founders Grotesk Semibold','Founders Grotesk Semibold Italic','Founders Grotesk Text','Founders Grotesk Text Bold','Founders Grotesk Text Bold Italic','Founders Grotesk Text Italic', 'Futura Bold','Futura Condensed ExtraBold','Futura Condensed Medium','Futura Medium','Futura Medium Italic', 'GB18030 Bitmap', 'Galvji','Galvji Bold','Galvji Bold Oblique','Galvji Oblique', 'Geeza Pro','Geeza Pro Bold', 'Geneva', 'Georgia','Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Gill Sans','Gill Sans Bold','Gill Sans Bold Italic','Gill Sans Italic','Gill Sans Light','Gill Sans Light Italic','Gill Sans SemiBold','Gill Sans SemiBold Italic','Gill Sans UltraBold', 'Gotu', 'Grantha Sangam MN','Grantha Sangam MN Black','Grantha Sangam MN Bold','Grantha Sangam MN DemiBold','Grantha Sangam MN Light','Grantha Sangam MN Medium', 'Graphik','Graphik Bold','Graphik Bold Italic','Graphik Compact','Graphik Compact Bold','Graphik Compact Bold Italic','Graphik Compact Medium','Graphik Compact Medium Italic','Graphik Compact Italic','Graphik Compact Semibold','Graphik Compact Semibold Italic','Graphik Light','Graphik Light Italic','Graphik Medium','Graphik Medium Italic','Graphik Italic','Graphik Semibold','Graphik Semibold Italic', 'Gujarati MT','Gujarati MT Bold','Gujarati Sangam MN','Gujarati Sangam MN Bold', 'GungSeo', 'Gurmukhi MN','Gurmukhi MN Bold','Gurmukhi MT','Gurmukhi Sangam MN','Gurmukhi Sangam MN Bold', 'Hannotate SC','Hannotate SC Bold','Hannotate TC','Hannotate TC Bold', 'HanziPen SC','HanziPen SC Bold','HanziPen TC','HanziPen TC Bold', 'HeadLineA', 'Hei', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Helvetica','Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Neue','Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Condensed Black','Helvetica Neue Condensed Bold','Helvetica Neue Italic','Helvetica Neue Light','Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin','Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic','Helvetica Oblique', 'Herculanum', 'Hiragino Maru Gothic ProN W4','Hiragino Mincho ProN W3','Hiragino Mincho ProN W6','Hiragino Sans CNS W3','Hiragino Sans CNS W6','Hiragino Sans GB W3','Hiragino Sans GB W6','Hiragino Sans TC W3','Hiragino Sans TC W6','Hiragino Sans W0','Hiragino Sans W1','Hiragino Sans W2','Hiragino Sans W3','Hiragino Sans W4','Hiragino Sans W5','Hiragino Sans W6','Hiragino Sans W7','Hiragino Sans W8','Hiragino Sans W9', 'Hoefler Text','Hoefler Text Black','Hoefler Text Black Italic','Hoefler Text Italic','Hoefler Text Ornaments', 'Hubballi', 'ITF Devanagari Bold','ITF Devanagari Book','ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Marathi Bold','ITF Devanagari Marathi Book','ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium','ITF Devanagari Medium', 'Impact', 'InaiMathi','InaiMathi Bold', 'Jaini','Jaini Purva', 'K2D','K2D Bold','K2D Bold Italic','K2D ExtraBold','K2D ExtraBold Italic','K2D ExtraLight','K2D ExtraLight Italic','K2D Italic','K2D Light','K2D Light Italic','K2D Medium','K2D Medium Italic','K2D SemiBold','K2D SemiBold Italic','K2D Thin','K2D Thin Italic', 'Kai', 'Kailasa','Kailasa Bold', 'Kaiti SC','Kaiti SC Black','Kaiti SC Bold','Kaiti TC','Kaiti TC Black','Kaiti TC Bold', 'Kannada MN','Kannada MN Bold','Kannada Sangam MN','Kannada Sangam MN Bold', 'Katari','Katari Black','Katari Black Italic','Katari Bold','Katari Bold Italic','Katari Italic','Katari Medium','Katari Medium Italic', 'Kavivanar', 'Kefa','Kefa Bold', 'Khmer MN','Khmer MN Bold','Khmer Sangam MN', 'Kigelia','Kigelia Arabic','Kigelia Arabic Bold','Kigelia Arabic Extrabold','Kigelia Arabic Light','Kigelia Arabic Semibold','Kigelia Bold','Kigelia Bold Italic','Kigelia Extrabold','Kigelia Extrabold Italic','Kigelia Italic','Kigelia Light','Kigelia Light Italic','Kigelia Semibold','Kigelia Semibold Italic', 'Klee Demibold','Klee Medium', 'KoHo','KoHo Bold','KoHo Bold Italic','KoHo ExtraLight','KoHo ExtraLight Italic','KoHo Italic','KoHo Light','KoHo Light Italic','KoHo Medium','KoHo Medium Italic','KoHo SemiBold','KoHo SemiBold Italic', 'Kodchasan','Kodchasan Bold','Kodchasan Bold Italic','Kodchasan ExtraLight','Kodchasan ExtraLight Italic','Kodchasan Italic','Kodchasan Light','Kodchasan Light Italic','Kodchasan Medium','Kodchasan Medium Italic','Kodchasan SemiBold','Kodchasan SemiBold Italic', 'Kohinoor Bangla','Kohinoor Bangla Bold','Kohinoor Bangla Light','Kohinoor Bangla Medium','Kohinoor Bangla Semibold','Kohinoor Devanagari','Kohinoor Devanagari Bold','Kohinoor Devanagari Light','Kohinoor Devanagari Medium','Kohinoor Devanagari Semibold','Kohinoor Gujarati','Kohinoor Gujarati Bold','Kohinoor Gujarati Light','Kohinoor Gujarati Medium','Kohinoor Gujarati Semibold','Kohinoor Telugu','Kohinoor Telugu Bold','Kohinoor Telugu Light','Kohinoor Telugu Medium','Kohinoor Telugu Semibold', 'Kokonor', 'Krub','Krub Bold','Krub Bold Italic','Krub ExtraLight','Krub ExtraLight Italic','Krub Italic','Krub Light','Krub Light Italic','Krub Medium','Krub Medium Italic','Krub SemiBold','Krub SemiBold Italic', 'Krungthep', 'KufiStandardGK', 'Lahore Gurmukhi','Lahore Gurmukhi Bold','Lahore Gurmukhi Light','Lahore Gurmukhi Medium','Lahore Gurmukhi SemiBold', 'Lantinghei SC Demibold','Lantinghei SC Extralight','Lantinghei SC Heavy','Lantinghei TC Demibold','Lantinghei TC Heavy', 'Lao MN','Lao MN Bold','Lao Sangam MN', 'Lava Devanagari','Lava Devanagari Bold','Lava Devanagari Heavy','Lava Devanagari Medium','Lava Kannada','Lava Kannada Bold','Lava Kannada Heavy','Lava Kannada Medium','Lava Telugu','Lava Telugu Bold','Lava Telugu Heavy','Lava Telugu Medium', 'LiHei Pro', 'LiSong Pro', 'Libian SC','Libian TC', 'LingWai SC Medium','LingWai TC Medium', 'Lucida Grande','Lucida Grande Bold', 'Luminari', 'Maku','Maku Bold', 'Malayalam MN','Malayalam MN Bold','Malayalam Sangam MN','Malayalam Sangam MN Bold', 'Mali','Mali Bold','Mali Bold Italic','Mali ExtraLight','Mali ExtraLight Italic','Mali Italic','Mali Light','Mali Light Italic','Mali Medium','Mali Medium Italic','Mali SemiBold','Mali SemiBold Italic', 'Marker Felt Thin','Marker Felt Wide', 'Menlo','Menlo Bold','Menlo Bold Italic','Menlo Italic', 'Microsoft Sans Serif', 'Mishafi','Mishafi Gold', 'Modak', 'Monaco', 'Mshtakan','Mshtakan Bold','Mshtakan BoldOblique','Mshtakan Oblique', 'Mukta','Mukta Bold','Mukta ExtraBold','Mukta ExtraLight','Mukta Light','Mukta Malar','Mukta Malar Bold','Mukta Malar ExtraBold','Mukta Malar ExtraLight','Mukta Malar Light','Mukta Malar Medium','Mukta Malar SemiBold','Mukta Medium','Mukta SemiBold','Mukta Vaani','Mukta Vaani Bold','Mukta Vaani ExtraBold','Mukta Vaani ExtraLight','Mukta Vaani Light','Mukta Vaani Medium','Mukta Vaani SemiBold', 'Mukta Mahee','Mukta Mahee Bold','Mukta Mahee ExtraBold','Mukta Mahee ExtraLight','Mukta Mahee Light','Mukta Mahee Medium','Mukta Mahee SemiBold', 'Muna','Muna Black','Muna Bold', 'Myanmar MN','Myanmar MN Bold','Myanmar Sangam MN','Myanmar Sangam MN Bold', 'Myriad Arabic','Myriad Arabic Black','Myriad Arabic Black Italic','Myriad Arabic Bold','Myriad Arabic Bold Italic','Myriad Arabic Italic','Myriad Arabic Light','Myriad Arabic Light Italic','Myriad Arabic Semibold','Myriad Arabic Semibold Italic', 'Nadeem', 'Nanum Brush Script','Nanum Pen Script','Nanum Gothic','Nanum Gothic Bold','Nanum Gothic ExtraBold','Nanum Myeongjo','Nanum Myeongjo Bold','Nanum Myeongjo ExtraBold', 'New Peninim MT','New Peninim MT Bold','New Peninim MT Bold Inclined','New Peninim MT Inclined', 'Niramit','Niramit Bold','Niramit Bold Italic','Niramit ExtraLight','Niramit ExtraLight Italic','Niramit Italic','Niramit Light','Niramit Light Italic','Niramit Medium','Niramit Medium Italic','Niramit SemiBold','Niramit SemiBold Italic', 'Nom Na Tong', 'Noteworthy Bold','Noteworthy Light', 'Noto Nastaliq Urdu','Noto Nastaliq Urdu Bold', 'Noto Sans Batak','Noto Sans Kannada','Noto Sans Kannada Black','Noto Sans Kannada Bold','Noto Sans Kannada ExtraBold','Noto Sans Kannada ExtraLight','Noto Sans Kannada Light','Noto Sans Kannada Medium','Noto Sans Kannada SemiBold','Noto Sans Kannada Thin','Noto Sans Myanmar','Noto Sans Myanmar Black','Noto Sans Myanmar Bold','Noto Sans Myanmar ExtraBold','Noto Sans Myanmar ExtraLight','Noto Sans Myanmar Light','Noto Sans Myanmar Medium','Noto Sans Myanmar SemiBold','Noto Sans Myanmar Thin','Noto Sans NKo','Noto Sans Oriya','Noto Sans Oriya Bold','Noto Sans Syriac','Noto Sans Syriac Black','Noto Sans Syriac Bold','Noto Sans Syriac ExtraBold','Noto Sans Syriac ExtraLight','Noto Sans Syriac Light','Noto Sans Syriac Medium','Noto Sans Syriac SemiBold','Noto Sans Syriac Thin','Noto Sans Tagalog', 'Noto Serif Kannada','Noto Serif Kannada Black','Noto Serif Kannada Bold','Noto Serif Kannada ExtraBold','Noto Serif Kannada ExtraLight','Noto Serif Kannada Light','Noto Serif Kannada Medium','Noto Serif Kannada SemiBold','Noto Serif Kannada Thin','Noto Serif Myanmar','Noto Serif Myanmar Black','Noto Serif Myanmar Bold','Noto Serif Myanmar ExtraBold','Noto Serif Myanmar ExtraLight','Noto Serif Myanmar Light','Noto Serif Myanmar Medium','Noto Serif Myanmar SemiBold','Noto Serif Myanmar Thin', 'November Bangla Traditional','November Bangla Traditional Black','November Bangla Traditional Bold','November Bangla Traditional Compressed','November Bangla Traditional Compressed Black','November Bangla Traditional Compressed Bold','November Bangla Traditional Compressed Extralight','November Bangla Traditional Compressed Hairline','November Bangla Traditional Compressed Heavy','November Bangla Traditional Compressed Light','November Bangla Traditional Compressed Medium','November Bangla Traditional Compressed Thin','November Bangla Traditional Condensed','November Bangla Traditional Condensed Black','November Bangla Traditional Condensed Bold','November Bangla Traditional Condensed Extralight','November Bangla Traditional Condensed Hairline','November Bangla Traditional Condensed Heavy','November Bangla Traditional Condensed Light','November Bangla Traditional Condensed Medium','November Bangla Traditional Condensed Thin','November Bangla Traditional Extralight','November Bangla Traditional Hairline','November Bangla Traditional Heavy','November Bangla Traditional Light','November Bangla Traditional Medium','November Bangla Traditional Thin', 'October Compressed Devanagari','October Compressed Devanagari Black','October Compressed Devanagari Bold','October Compressed Devanagari ExtraLight','October Compressed Devanagari Hairline','October Compressed Devanagari Heavy','October Compressed Devanagari Light','October Compressed Devanagari Medium','October Compressed Devanagari Thin','October Compressed Gujarati','October Compressed Gujarati Black','October Compressed Gujarati Bold','October Compressed Gujarati ExtraLight','October Compressed Gujarati Hairline','October Compressed Gujarati Heavy','October Compressed Gujarati Light','October Compressed Gujarati Medium','October Compressed Gujarati Thin','October Compressed Gurmukhi','October Compressed Gurmukhi Black','October Compressed Gurmukhi Bold','October Compressed Gurmukhi ExtraLight','October Compressed Gurmukhi Hairline','October Compressed Gurmukhi Heavy','October Compressed Gurmukhi Light','October Compressed Gurmukhi Medium','October Compressed Gurmukhi Thin','October Compressed Kannada','October Compressed Kannada Black','October Compressed Kannada Bold','October Compressed Kannada ExtraLight','October Compressed Kannada Hairline','October Compressed Kannada Heavy','October Compressed Kannada Light','October Compressed Kannada Medium','October Compressed Kannada Thin','October Compressed Meetei Mayek','October Compressed Meetei Mayek Black','October Compressed Meetei Mayek Bold','October Compressed Meetei Mayek ExtraLight','October Compressed Meetei Mayek Hairline','October Compressed Meetei Mayek Heavy','October Compressed Meetei Mayek Light','October Compressed Meetei Mayek Medium','October Compressed Meetei Mayek Thin','October Compressed Odia','October Compressed Odia Black','October Compressed Odia Bold','October Compressed Odia ExtraLight','October Compressed Odia Hairline','October Compressed Odia Heavy','October Compressed Odia Light','October Compressed Odia Medium','October Compressed Odia Thin','October Compressed Ol Chiki','October Compressed Ol Chiki Black','October Compressed Ol Chiki Bold','October Compressed Ol Chiki ExtraLight','October Compressed Ol Chiki Hairline','October Compressed Ol Chiki Heavy','October Compressed Ol Chiki Light','October Compressed Ol Chiki Medium','October Compressed Ol Chiki Thin','October Compressed Tamil','October Compressed Tamil Black','October Compressed Tamil Bold','October Compressed Tamil ExtraLight','October Compressed Tamil Hairline','October Compressed Tamil Heavy','October Compressed Tamil Light','October Compressed Tamil Medium','October Compressed Tamil Thin','October Compressed Telugu','October Compressed Telugu Black','October Compressed Telugu Bold','October Compressed Telugu ExtraLight','October Compressed Telugu Hairline','October Compressed Telugu Heavy','October Compressed Telugu Light','October Compressed Telugu Medium','October Compressed Telugu Thin','October Condensed Devanagari','October Condensed Devanagari Black','October Condensed Devanagari Bold','October Condensed Devanagari ExtraLight','October Condensed Devanagari Hairline','October Condensed Devanagari Heavy','October Condensed Devanagari Light','October Condensed Devanagari Medium','October Condensed Devanagari Thin','October Condensed Gujarati','October Condensed Gujarati Black','October Condensed Gujarati Bold','October Condensed Gujarati ExtraLight','October Condensed Gujarati Hairline','October Condensed Gujarati Heavy','October Condensed Gujarati Light','October Condensed Gujarati Medium','October Condensed Gujarati Thin','October Condensed Gurmukhi','October Condensed Gurmukhi Black','October Condensed Gurmukhi Bold','October Condensed Gurmukhi ExtraLight','October Condensed Gurmukhi Hairline','October Condensed Gurmukhi Heavy','October Condensed Gurmukhi Light','October Condensed Gurmukhi Medium','October Condensed Gurmukhi Thin','October Condensed Kannada','October Condensed Kannada Black','October Condensed Kannada Bold','October Condensed Kannada ExtraLight','October Condensed Kannada Hairline','October Condensed Kannada Heavy','October Condensed Kannada Light','October Condensed Kannada Medium','October Condensed Kannada Thin','October Condensed Meetei Mayek','October Condensed Meetei Mayek Black','October Condensed Meetei Mayek Bold','October Condensed Meetei Mayek ExtraLight','October Condensed Meetei Mayek Hairline','October Condensed Meetei Mayek Heavy','October Condensed Meetei Mayek Light','October Condensed Meetei Mayek Medium','October Condensed Meetei Mayek Thin','October Condensed Odia','October Condensed Odia Black','October Condensed Odia Bold','October Condensed Odia ExtraLight','October Condensed Odia Hairline','October Condensed Odia Heavy','October Condensed Odia Light','October Condensed Odia Medium','October Condensed Odia Thin','October Condensed Ol Chiki','October Condensed Ol Chiki Black','October Condensed Ol Chiki Bold','October Condensed Ol Chiki ExtraLight','October Condensed Ol Chiki Hairline','October Condensed Ol Chiki Heavy','October Condensed Ol Chiki Light','October Condensed Ol Chiki Medium','October Condensed Ol Chiki Thin','October Condensed Tamil','October Condensed Tamil Black','October Condensed Tamil Bold','October Condensed Tamil ExtraLight','October Condensed Tamil Hairline','October Condensed Tamil Heavy','October Condensed Tamil Light','October Condensed Tamil Medium','October Condensed Tamil Thin','October Condensed Telugu','October Condensed Telugu Black','October Condensed Telugu Bold','October Condensed Telugu ExtraLight','October Condensed Telugu Hairline','October Condensed Telugu Heavy','October Condensed Telugu Light','October Condensed Telugu Medium','October Condensed Telugu Thin','October Devanagari','October Devanagari Black','October Devanagari Bold','October Devanagari ExtraLight','October Devanagari Hairline','October Devanagari Heavy','October Devanagari Light','October Devanagari Medium','October Devanagari Thin','October Gujarati','October Gujarati Black','October Gujarati Bold','October Gujarati ExtraLight','October Gujarati Hairline','October Gujarati Heavy','October Gujarati Light','October Gujarati Medium','October Gujarati Thin','October Gurmukhi','October Gurmukhi Black','October Gurmukhi Bold','October Gurmukhi ExtraLight','October Gurmukhi Hairline','October Gurmukhi Heavy','October Gurmukhi Light','October Gurmukhi Medium','October Gurmukhi Thin','October Kannada','October Kannada Black','October Kannada Bold','October Kannada ExtraLight','October Kannada Hairline','October Kannada Heavy','October Kannada Light','October Kannada Medium','October Kannada Thin','October Meetei Mayek','October Meetei Mayek Black','October Meetei Mayek Bold','October Meetei Mayek ExtraLight','October Meetei Mayek Hairline','October Meetei Mayek Heavy','October Meetei Mayek Light','October Meetei Mayek Medium','October Meetei Mayek Thin','October Odia','October Odia Black','October Odia Bold','October Odia ExtraLight','October Odia Hairline','October Odia Heavy','October Odia Light','October Odia Medium','October Odia Thin','October Ol Chiki','October Ol Chiki Black','October Ol Chiki Bold','October Ol Chiki ExtraLight','October Ol Chiki Hairline','October Ol Chiki Heavy','October Ol Chiki Light','October Ol Chiki Medium','October Ol Chiki Thin','October Tamil','October Tamil Black','October Tamil Bold','October Tamil ExtraLight','October Tamil Hairline','October Tamil Heavy','October Tamil Light','October Tamil Medium','October Tamil Thin','October Telugu','October Telugu Black','October Telugu Bold','October Telugu ExtraLight','October Telugu Hairline','October Telugu Heavy','October Telugu Light','October Telugu Medium','October Telugu Thin', 'Optima','Optima Bold','Optima Bold Italic','Optima ExtraBlack','Optima Italic', 'Oriya MN','Oriya MN Bold','Oriya Sangam MN','Oriya Sangam MN Bold', 'Osaka','Osaka-Mono', 'PCMyungjo', 'PSL Ornanong Pro','PSL Ornanong Pro Bold','PSL Ornanong Pro Bold Italic','PSL Ornanong Pro Demibold','PSL Ornanong Pro Demibold Italic','PSL Ornanong Pro Italic','PSL Ornanong Pro Light','PSL Ornanong Pro Light Italic', 'PT Mono','PT Mono Bold','PT Sans','PT Sans Bold','PT Sans Bold Italic','PT Sans Caption','PT Sans Caption Bold','PT Sans Italic','PT Sans Narrow','PT Sans Narrow Bold','PT Serif','PT Serif Bold','PT Serif Bold Italic','PT Serif Caption','PT Serif Caption Italic','PT Serif Italic', 'Padyakke Expanded One', 'Palatino','Palatino Bold','Palatino Bold Italic','Palatino Italic', 'Papyrus','Papyrus Condensed', 'Party LET', 'Phosphate Inline','Phosphate Solid', 'PilGi', 'PingFang HK','PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight','PingFang MO','PingFang MO Light','PingFang MO Medium','PingFang MO Semibold','PingFang MO Thin','PingFang MO Ultralight','PingFang SC','PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight','PingFang TC','PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Plantagenet Cherokee', 'Produkt','Produkt Extralight','Produkt Extralight Italic','Produkt Light','Produkt Light Italic','Produkt Medium','Produkt Medium Italic','Produkt Italic', 'Proxima Nova','Proxima Nova Bold','Proxima Nova Bold It','Proxima Nova Extrabold','Proxima Nova Extrabold It','Proxima Nova It','Proxima Nova Light','Proxima Nova Light It','Proxima Nova Medium','Proxima Nova Medium It','Proxima Nova Semibold','Proxima Nova Semibold It', 'Publico Headline Black','Publico Headline Black Italic','Publico Headline Bold','Publico Headline Bold Italic','Publico Headline Italic','Publico Headline Roman','Publico Text Bold','Publico Text Bold Italic','Publico Text Italic','Publico Text Roman','Publico Text Semibold','Publico Text Semibold Italic', 'Quotes Caps','Quotes Script', 'Raanana','Raanana Bold', 'Rockwell','Rockwell Bold','Rockwell Bold Italic','Rockwell Italic', 'STFangsong', 'STHeiti', 'STIX Two Math','STIX Two Text','STIX Two Text Bold','STIX Two Text Bold Italic','STIX Two Text Italic','STIX Two Text Medium','STIX Two Text Medium Italic','STIX Two Text SemiBold','STIX Two Text SemiBold Italic', 'STKaiti', 'STSong', 'STXihei', 'Sama Devanagari','Sama Devanagari Bold','Sama Devanagari Book','Sama Devanagari ExtraBold','Sama Devanagari Medium','Sama Devanagari SemiBold','Sama Gujarati','Sama Gujarati Bold','Sama Gujarati Book','Sama Gujarati ExtraBold','Sama Gujarati Medium','Sama Gujarati SemiBold','Sama Gurmukhi','Sama Gurmukhi Bold','Sama Gurmukhi Book','Sama Gurmukhi ExtraBold','Sama Gurmukhi Medium','Sama Gurmukhi SemiBold','Sama Kannada','Sama Kannada Bold','Sama Kannada Book','Sama Kannada ExtraBold','Sama Kannada Medium','Sama Kannada SemiBold','Sama Malayalam','Sama Malayalam Bold','Sama Malayalam Book','Sama Malayalam ExtraBold','Sama Malayalam Medium','Sama Malayalam SemiBold','Sama Tamil','Sama Tamil Bold','Sama Tamil Book','Sama Tamil ExtraBold','Sama Tamil Medium','Sama Tamil SemiBold', 'Sana', 'Sarabun','Sarabun Bold','Sarabun Bold Italic','Sarabun ExtraBold','Sarabun ExtraBold Italic','Sarabun ExtraLight','Sarabun ExtraLight Italic','Sarabun Italic','Sarabun Light','Sarabun Light Italic','Sarabun Medium','Sarabun Medium Italic','Sarabun SemiBold','Sarabun SemiBold Italic','Sarabun Thin','Sarabun Thin Italic', 'Sathu', 'Sauber Script', 'Savoye LET', 'Shobhika','Shobhika Bold', 'Shree Devanagari 714','Shree Devanagari 714 Bold','Shree Devanagari 714 Bold Italic','Shree Devanagari 714 Italic', 'SignPainter','SignPainter Semibold', 'Silom', 'SimSong','SimSong Bold', 'Sinhala MN','Sinhala MN Bold','Sinhala Sangam MN','Sinhala Sangam MN Bold', 'Skia','Skia Black','Skia Black Condensed','Skia Black Extended','Skia Bold','Skia Condensed','Skia Extended','Skia Light','Skia Light Condensed','Skia Light Extended', 'Snell Roundhand','Snell Roundhand Black','Snell Roundhand Bold', 'Songti SC','Songti SC Black','Songti SC Bold','Songti SC Light','Songti TC','Songti TC Bold','Songti TC Light', 'Spot Mono','Spot Mono Bold','Spot Mono Medium', 'Srisakdi','Srisakdi Bold', 'Sukhumvit Set Bold','Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Semi Bold','Sukhumvit Set Text','Sukhumvit Set Thin', 'Symbol', 'Tahoma','Tahoma Bold', 'Tamil MN','Tamil MN Bold','Tamil Sangam MN','Tamil Sangam MN Black','Tamil Sangam MN Bold','Tamil Sangam MN Demibold','Tamil Sangam MN Light','Tamil Sangam MN Medium', 'Telugu MN','Telugu MN Bold','Telugu Sangam MN','Telugu Sangam MN Bold', 'Thonburi','Thonburi Bold','Thonburi Light', 'Times New Roman','Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Tiro Bangla','Tiro Bangla Italic','Tiro Devanagari Hindi','Tiro Devanagari Hindi Italic','Tiro Devanagari Marathi','Tiro Devanagari Marathi Italic','Tiro Devanagari Sanskrit','Tiro Devanagari Sanskrit Italic','Tiro Gurmukhi','Tiro Gurmukhi Italic','Tiro Kannada','Tiro Kannada Italic','Tiro Tamil','Tiro Tamil Italic','Tiro Telugu','Tiro Telugu Italic', 'Toppan Bunkyu Gothic','Toppan Bunkyu Gothic Demibold','Toppan Bunkyu Midashi Gothic Extrabold','Toppan Bunkyu Midashi Mincho Extrabold','Toppan Bunkyu Mincho', 'Trattatello', 'Trebuchet MS','Trebuchet MS Bold','Trebuchet MS Bold Italic','Trebuchet MS Italic', 'Tsukushi A Round Gothic','Tsukushi A Round Gothic Bold','Tsukushi B Round Gothic','Tsukushi B Round Gothic Bold', 'Verdana','Verdana Bold','Verdana Bold Italic','Verdana Italic', 'Waseem','Waseem Light', 'Wawati SC','Wawati TC', 'Webdings', 'Weibei SC Bold','Weibei TC Bold', 'Wingdings','Wingdings 2','Wingdings 3', 'Xingkai SC Bold','Xingkai SC Light','Xingkai TC Bold','Xingkai TC Light', 'YuGothic Bold','YuGothic Medium', 'YuKyokasho Bold','YuKyokasho Medium','YuKyokasho Yoko Bold','YuKyokasho Yoko Medium', 'YuMincho +36p Kana Demibold','YuMincho +36p Kana Extrabold','YuMincho +36p Kana Medium','YuMincho Demibold','YuMincho Extrabold','YuMincho Medium', 'Yuanti SC','Yuanti SC Bold','Yuanti SC Light','Yuanti TC','Yuanti TC Bold','Yuanti TC Light', 'Yuppy SC','Yuppy TC', 'Zapf Dingbats', 'Zapfino', ] }, 'v26': { name: 'Tahoe', version: 26, url: 'https://support.apple.com/en-us/122869', list: [ 'Academy Engraved LET', 'Adelle Sans Devanagari','Adelle Sans Devanagari Bold','Adelle Sans Devanagari Extrabold','Adelle Sans Devanagari Heavy','Adelle Sans Devanagari Light','Adelle Sans Devanagari Semibold','Adelle Sans Devanagari Thin', 'AkayaKanadaka','AkayaTelivigala', 'Al Bayan','Al Bayan Bold','Al Nile','Al Nile Bold','Al Tarikh', 'American Typewriter','American Typewriter Bold','American Typewriter Condensed','American Typewriter Condensed Bold','American Typewriter Condensed Light','American Typewriter Light','American Typewriter Semibold', 'Andale Mono', 'Annai MN', 'Apple Braille','Apple Braille Outline 6 Dot','Apple Braille Outline 8 Dot','Apple Braille Pinpoint 6 Dot','Apple Braille Pinpoint 8 Dot', 'Apple Chancery','Apple Color Emoji','Apple LiGothic Medium', 'Apple LiSung Light','Apple SD Gothic Neo','Apple SD Gothic Neo Bold','Apple SD Gothic Neo ExtraBold','Apple SD Gothic Neo Heavy','Apple SD Gothic Neo Light','Apple SD Gothic Neo Medium','Apple SD Gothic Neo SemiBold','Apple SD Gothic Neo Thin','Apple SD Gothic Neo UltraLight','Apple Symbols','AppleGothic','AppleMyungjo', 'Arial','Arial Black','Arial Bold','Arial Bold Italic','Arial Hebrew','Arial Hebrew Bold','Arial Hebrew Light','Arial Hebrew Scholar','Arial Hebrew Scholar Bold','Arial Hebrew Scholar Light','Arial Italic','Arial Narrow','Arial Narrow Bold','Arial Narrow Bold Italic','Arial Narrow Italic','Arial Rounded MT Bold','Arial Unicode MS', 'Arima Koshi','Arima Koshi Black','Arima Koshi Bold','Arima Koshi ExtraBold','Arima Koshi ExtraLight','Arima Koshi Light','Arima Koshi Medium','Arima Koshi Thin','Arima Madurai','Arima Madurai Black','Arima Madurai Bold','Arima Madurai ExtraLight','Arima Madurai Light','Arima Madurai Medium','Arima Madurai Semi Bold','Arima Madurai Thin', 'Avenir Black','Avenir Black Oblique','Avenir Book','Avenir Book Oblique','Avenir Heavy','Avenir Heavy Oblique','Avenir Light','Avenir Light Oblique','Avenir Medium','Avenir Medium Oblique','Avenir Next Bold','Avenir Next Bold Italic','Avenir Next Condensed','Avenir Next Condensed Bold','Avenir Next Condensed Bold Italic','Avenir Next Condensed Demi Bold','Avenir Next Condensed Demi Bold Italic','Avenir Next Condensed Heavy','Avenir Next Condensed Heavy Italic','Avenir Next Condensed Italic','Avenir Next Condensed Medium','Avenir Next Condensed Medium Italic','Avenir Next Condensed Ultra Light','Avenir Next Condensed Ultra Light Italic','Avenir Next','Avenir Next Demi Bold','Avenir Next Demi Bold Italic','Avenir Next Heavy','Avenir Next Heavy Italic','Avenir Next Italic','Avenir Next Medium','Avenir Next Medium Italic','Avenir Next Ultra Light','Avenir Next Ultra Light Italic','Avenir Oblique','Avenir Roman', 'Ayuthaya', 'Baghdad', 'Bai Jamjuree','Bai Jamjuree Bold','Bai Jamjuree Bold Italic','Bai Jamjuree ExtraLight','Bai Jamjuree ExtraLight Italic','Bai Jamjuree Italic','Bai Jamjuree Light','Bai Jamjuree Light Italic','Bai Jamjuree Medium','Bai Jamjuree Medium Italic','Bai Jamjuree SemiBold','Bai Jamjuree SemiBold Italic', 'BIZ UDGothic','BIZ UDGothic Bold','BIZ UDMincho', 'BM DoHyeon','BM Hanna 11yrs Old','BM Hanna Air','BM Hanna Pro','BM Jua','BM Kirang Haerang','BM Yeonsung', 'Baloo 2','Baloo 2 Bold','Baloo 2 ExtraBold','Baloo 2 Medium','Baloo 2 SemiBold','Baloo Bhai 2','Baloo Bhai 2 Bold','Baloo Bhai 2 ExtraBold','Baloo Bhai 2 Medium','Baloo Bhai 2 SemiBold','Baloo Bhaijaan','Baloo Bhaina 2','Baloo Bhaina 2 Bold','Baloo Bhaina 2 ExtraBold','Baloo Bhaina 2 Medium','Baloo Bhaina 2 SemiBold','Baloo Chettan 2','Baloo Chettan 2 Bold','Baloo Chettan 2 ExtraBold','Baloo Chettan 2 Medium','Baloo Chettan 2 SemiBold','Baloo Da 2','Baloo Da 2 Bold','Baloo Da 2 ExtraBold','Baloo Da 2 Medium','Baloo Da 2 SemiBold','Baloo Paaji 2','Baloo Paaji 2 Bold','Baloo Paaji 2 ExtraBold','Baloo Paaji 2 Medium','Baloo Paaji 2 SemiBold','Baloo Tamma 2','Baloo Tamma 2 Bold','Baloo Tamma 2 ExtraBold','Baloo Tamma 2 Medium','Baloo Tamma 2 SemiBold','Baloo Tammudu 2','Baloo Tammudu 2 Bold','Baloo Tammudu 2 ExtraBold','Baloo Tammudu 2 Medium','Baloo Tammudu 2 SemiBold','Baloo Thambi 2','Baloo Thambi 2 Bold','Baloo Thambi 2 ExtraBold','Baloo Thambi 2 Medium','Baloo Thambi 2 SemiBold', 'Bangla MN','Bangla MN Bold','Bangla Sangam MN','Bangla Sangam MN Bold', 'Baoli SC','Baoli TC', 'Baskerville','Baskerville Bold','Baskerville Bold Italic','Baskerville Italic','Baskerville SemiBold','Baskerville SemiBold Italic', 'Beirut', 'BiauKaiHK','BiauKaiTC', 'Big Caslon Medium', 'Bodoni 72 Bold','Bodoni 72 Book','Bodoni 72 Book Italic','Bodoni 72 Oldstyle Bold','Bodoni 72 Oldstyle Book','Bodoni 72 Oldstyle Book Italic','Bodoni 72 Smallcaps Book','Bodoni Ornaments', 'Bradley Hand Bold', 'Brill Bold Italic','Brill Italic','Brill Medium Italic','Brill Roman','Brill Roman Bold','Brill Roman Medium','Brill Roman Semibold','Brill Semibold Italic', 'Brush Script MT Italic', 'Cambay Devanagari','Cambay Devanagari Bold','Cambay Devanagari Bold Oblique','Cambay Devanagari Oblique', 'Canela','Canela Bold','Canela Bold Italic','Canela Deck','Canela Deck Bold','Canela Deck Bold Italic','Canela Deck Italic','Canela Deck Medium','Canela Deck Medium Italic','Canela Italic','Canela Text','Canela Text Bold','Canela Text Bold Italic','Canela Text Italic','Canela Text Medium','Canela Text Medium Italic', 'Catamaran','Catamaran Black','Catamaran Bold','Catamaran ExtraBold','Catamaran ExtraLight','Catamaran Light','Catamaran Medium','Catamaran SemiBold','Catamaran Thin', 'Chakra Petch','Chakra Petch Bold','Chakra Petch Bold Italic','Chakra Petch ExtraLight','Chakra Petch ExtraLight Italic','Chakra Petch Italic','Chakra Petch Light','Chakra Petch Light Italic','Chakra Petch Medium','Chakra Petch Medium Italic','Chakra Petch SemiBold','Chakra Petch SemiBold Italic', 'Chalkboard','Chalkboard Bold','Chalkboard SE','Chalkboard SE Bold','Chalkboard SE Light','Chalkduster', 'Charm','Charm Bold', 'Charmonman','Charmonman Bold', 'Charter Black','Charter Black Italic','Charter Bold','Charter Bold Italic','Charter Italic','Charter Roman', 'Cochin','Cochin Bold','Cochin Bold Italic','Cochin Italic', 'Comic Sans MS','Comic Sans MS Bold', 'Copperplate','Copperplate Bold','Copperplate Light', 'Corsiva Hebrew','Corsiva Hebrew Bold', 'Courier New','Courier New Bold','Courier New Bold Italic','Courier New Italic', 'DIN Alternate Bold','DIN Condensed Bold', 'Damascus','Damascus Bold','Damascus Light','Damascus Medium','Damascus Semi Bold', 'Dash','Dash Practice', 'DecoType Naskh', 'Devanagari MT','Devanagari MT Bold','Devanagari Sangam MN','Devanagari Sangam MN Bold', 'Didot','Didot Bold','Didot Italic', 'Diwan Kufi','Diwan Thuluth', 'Domaine Display','Domaine Display Bold','Domaine Display Bold Italic','Domaine Display Italic','Domaine Display Medium','Domaine Display Medium Italic', 'Euphemia UCAS','Euphemia UCAS Bold','Euphemia UCAS Italic', 'Fahkwang','Fahkwang Bold','Fahkwang Bold Italic','Fahkwang ExtraLight','Fahkwang ExtraLight Italic','Fahkwang Italic','Fahkwang Light','Fahkwang Light Italic','Fahkwang Medium','Fahkwang Medium Italic','Fahkwang SemiBold','Fahkwang SemiBold Italic', 'Farah', 'Farisi', 'Founders Grotesk','Founders Grotesk Bold','Founders Grotesk Bold Italic','Founders Grotesk Condensed','Founders Grotesk Condensed Bold','Founders Grotesk Condensed Semibold','Founders Grotesk Italic','Founders Grotesk Light','Founders Grotesk Light Italic','Founders Grotesk Medium','Founders Grotesk Medium Italic','Founders Grotesk Semibold','Founders Grotesk Semibold Italic','Founders Grotesk Text','Founders Grotesk Text Bold','Founders Grotesk Text Bold Italic','Founders Grotesk Text Italic', 'Futura Bold','Futura Condensed ExtraBold','Futura Condensed Medium','Futura Medium','Futura Medium Italic', 'GB18030 Bitmap', 'Galvji','Galvji Bold','Galvji Bold Oblique','Galvji Oblique', 'Geeza Pro','Geeza Pro Bold', 'Geneva', 'Georgia','Georgia Bold','Georgia Bold Italic','Georgia Italic', 'Gill Sans','Gill Sans Bold','Gill Sans Bold Italic','Gill Sans Italic','Gill Sans Light','Gill Sans Light Italic','Gill Sans SemiBold','Gill Sans SemiBold Italic','Gill Sans UltraBold', 'Gotu', 'Grantha Sangam MN','Grantha Sangam MN Black','Grantha Sangam MN Bold','Grantha Sangam MN DemiBold','Grantha Sangam MN Light','Grantha Sangam MN Medium', 'Graphik','Graphik Bold','Graphik Bold Italic','Graphik Compact','Graphik Compact Bold','Graphik Compact Bold Italic','Graphik Compact Italic','Graphik Compact Medium','Graphik Compact Medium Italic','Graphik Compact Semibold','Graphik Compact Semibold Italic','Graphik Italic','Graphik Light','Graphik Light Italic','Graphik Medium','Graphik Medium Italic','Graphik Semibold','Graphik Semibold Italic', 'Gujarati MT','Gujarati MT Bold','Gujarati Sangam MN','Gujarati Sangam MN Bold', 'GungSeo', 'Gurmukhi MN','Gurmukhi MN Bold','Gurmukhi MT','Gurmukhi Sangam MN','Gurmukhi Sangam MN Bold', 'Hannotate SC','Hannotate SC Bold','Hannotate TC','Hannotate TC Bold', 'HanziPen SC','HanziPen SC Bold','HanziPen TC','HanziPen TC Bold', 'HeadLineA', 'Hei', 'Heiti SC Light','Heiti SC Medium','Heiti TC Light','Heiti TC Medium', 'Helvetica','Helvetica Bold','Helvetica Bold Oblique','Helvetica Light','Helvetica Light Oblique','Helvetica Neue','Helvetica Neue Bold','Helvetica Neue Bold Italic','Helvetica Neue Condensed Black','Helvetica Neue Condensed Bold','Helvetica Neue Italic','Helvetica Neue Light','Helvetica Neue Light Italic','Helvetica Neue Medium','Helvetica Neue Medium Italic','Helvetica Neue Thin','Helvetica Neue Thin Italic','Helvetica Neue UltraLight','Helvetica Neue UltraLight Italic','Helvetica Oblique', 'Herculanum', 'Hiragino Maru Gothic ProN W4','Hiragino Mincho ProN W3','Hiragino Mincho ProN W6','Hiragino Sans GB W3','Hiragino Sans GB W6','Hiragino Sans TC W3','Hiragino Sans TC W6','Hiragino Sans W0','Hiragino Sans W1','Hiragino Sans W2','Hiragino Sans W3','Hiragino Sans W4','Hiragino Sans W5','Hiragino Sans W6','Hiragino Sans W7','Hiragino Sans W8','Hiragino Sans W9', 'Hoefler Text','Hoefler Text Black','Hoefler Text Black Italic','Hoefler Text Italic','Hoefler Text Ornaments', 'Hubballi', 'Impact', 'InaiMathi','InaiMathi Bold', 'ITF Devanagari Bold','ITF Devanagari Book','ITF Devanagari Demi','ITF Devanagari Light','ITF Devanagari Marathi Bold','ITF Devanagari Marathi Book','ITF Devanagari Marathi Demi','ITF Devanagari Marathi Light','ITF Devanagari Marathi Medium','ITF Devanagari Medium', 'Jaini','Jaini Purva', 'K2D','K2D Bold','K2D Bold Italic','K2D ExtraBold','K2D ExtraBold Italic','K2D ExtraLight','K2D ExtraLight Italic','K2D Italic','K2D Light','K2D Light Italic','K2D Medium','K2D Medium Italic','K2D SemiBold','K2D SemiBold Italic','K2D Thin','K2D Thin Italic', 'Kai', 'Kailasa','Kailasa Bold', 'Kaiti SC','Kaiti SC Black','Kaiti SC Bold','Kaiti TC','Kaiti TC Black','Kaiti TC Bold', 'Kannada MN','Kannada MN Bold','Kannada Sangam MN','Kannada Sangam MN Bold', 'Katari','Katari Black','Katari Black Italic','Katari Bold','Katari Bold Italic','Katari Italic','Katari Medium','Katari Medium Italic', 'Kavivanar', 'Kefa III','Kefa III Bold','Kefa III ExtraBold','Kefa III Light', 'Khmer MN','Khmer MN Bold','Khmer Sangam MN', 'Kigelia','Kigelia Arabic','Kigelia Arabic Bold','Kigelia Arabic Extrabold','Kigelia Arabic Light','Kigelia Arabic Semibold','Kigelia Bold','Kigelia Bold Italic','Kigelia Extrabold','Kigelia Extrabold Italic','Kigelia Italic','Kigelia Light','Kigelia Light Italic','Kigelia Semibold','Kigelia Semibold Italic', 'Klee Demibold','Klee Medium', 'KoHo','KoHo Bold','KoHo Bold Italic','KoHo ExtraLight','KoHo ExtraLight Italic','KoHo Italic','KoHo Light','KoHo Light Italic','KoHo Medium','KoHo Medium Italic','KoHo SemiBold','KoHo SemiBold Italic', 'Kodchasan','Kodchasan Bold','Kodchasan Bold Italic','Kodchasan ExtraLight','Kodchasan ExtraLight Italic','Kodchasan Italic','Kodchasan Light','Kodchasan Light Italic','Kodchasan Medium','Kodchasan Medium Italic','Kodchasan SemiBold','Kodchasan SemiBold Italic', 'Kohinoor Bangla','Kohinoor Bangla Bold','Kohinoor Bangla Light','Kohinoor Bangla Medium','Kohinoor Bangla Semibold','Kohinoor Devanagari','Kohinoor Devanagari Bold','Kohinoor Devanagari Light','Kohinoor Devanagari Medium','Kohinoor Devanagari Semibold','Kohinoor Gujarati','Kohinoor Gujarati Bold','Kohinoor Gujarati Light','Kohinoor Gujarati Medium','Kohinoor Gujarati Semibold','Kohinoor Telugu','Kohinoor Telugu Bold','Kohinoor Telugu Light','Kohinoor Telugu Medium','Kohinoor Telugu Semibold', 'Kokonor', 'Krub','Krub Bold','Krub Bold Italic','Krub ExtraLight','Krub ExtraLight Italic','Krub Italic','Krub Light','Krub Light Italic','Krub Medium','Krub Medium Italic','Krub SemiBold','Krub SemiBold Italic', 'Krungthep', 'KufiStandardGK', 'Lahore Gurmukhi','Lahore Gurmukhi Bold','Lahore Gurmukhi Light','Lahore Gurmukhi Medium','Lahore Gurmukhi SemiBold', 'Lantinghei SC Demibold','Lantinghei SC Extralight','Lantinghei SC Heavy','Lantinghei TC Demibold','Lantinghei TC Heavy', 'Lao MN','Lao MN Bold','Lao Sangam MN', 'Lava Devanagari','Lava Devanagari Bold','Lava Devanagari Heavy','Lava Devanagari Medium','Lava Kannada','Lava Kannada Bold','Lava Kannada Heavy','Lava Kannada Medium','Lava Telugu','Lava Telugu Bold','Lava Telugu Heavy','Lava Telugu Medium', 'Libian SC','Libian TC', 'LiHei Pro', 'LingWai SC Medium','LingWai TC Medium', 'LiSong Pro', 'Lucida Grande','Lucida Grande Bold', 'Luminari', 'Maku','Maku Bold', 'Malayalam MN','Malayalam MN Bold','Malayalam Sangam MN','Malayalam Sangam MN Bold', 'Mali','Mali Bold','Mali Bold Italic','Mali ExtraLight','Mali ExtraLight Italic','Mali Italic','Mali Light','Mali Light Italic','Mali Medium','Mali Medium Italic','Mali SemiBold','Mali SemiBold Italic', 'Marker Felt Thin','Marker Felt Wide', 'Menlo','Menlo Bold','Menlo Bold Italic','Menlo Italic', 'Microsoft Sans Serif', 'Mishafi','Mishafi Gold', 'Modak', 'Monaco', 'Mshtakan','Mshtakan Bold','Mshtakan BoldOblique','Mshtakan Oblique', 'Mukta','Mukta Bold','Mukta ExtraBold','Mukta ExtraLight','Mukta Light','Mukta Mahee','Mukta Mahee Bold','Mukta Mahee ExtraBold','Mukta Mahee ExtraLight','Mukta Mahee Light','Mukta Mahee Medium','Mukta Mahee SemiBold','Mukta Malar','Mukta Malar Bold','Mukta Malar ExtraBold','Mukta Malar ExtraLight','Mukta Malar Light','Mukta Malar Medium','Mukta Malar SemiBold','Mukta Medium','Mukta SemiBold','Mukta Vaani','Mukta Vaani Bold','Mukta Vaani ExtraBold','Mukta Vaani ExtraLight','Mukta Vaani Light','Mukta Vaani Medium','Mukta Vaani SemiBold', 'Muna','Muna Black','Muna Bold', 'Myanmar MN','Myanmar MN Bold','Myanmar Sangam MN','Myanmar Sangam MN Bold', 'Myriad Arabic','Myriad Arabic Black','Myriad Arabic Black Italic','Myriad Arabic Bold','Myriad Arabic Bold Italic','Myriad Arabic Italic','Myriad Arabic Light','Myriad Arabic Light Italic','Myriad Arabic Semibold','Myriad Arabic Semibold Italic', 'Nadeem', 'Nanum Brush Script','Nanum Gothic','Nanum Gothic Bold','Nanum Gothic ExtraBold','Nanum Myeongjo','Nanum Myeongjo Bold','Nanum Myeongjo ExtraBold','Nanum Pen Script', 'New Peninim MT','New Peninim MT Bold','New Peninim MT Bold Inclined','New Peninim MT Inclined', 'Niramit','Niramit Bold','Niramit Bold Italic','Niramit ExtraLight','Niramit ExtraLight Italic','Niramit Italic','Niramit Light','Niramit Light Italic','Niramit Medium','Niramit Medium Italic','Niramit SemiBold','Niramit SemiBold Italic', 'Nom Na Tong', 'Noteworthy Bold','Noteworthy Light', 'Noto Nastaliq Urdu','Noto Nastaliq Urdu Bold', 'Noto Sans Batak','Noto Sans Kannada','Noto Sans Kannada Black','Noto Sans Kannada Bold','Noto Sans Kannada ExtraBold','Noto Sans Kannada ExtraLight','Noto Sans Kannada Light','Noto Sans Kannada Medium','Noto Sans Kannada SemiBold','Noto Sans Kannada Thin','Noto Sans Myanmar','Noto Sans Myanmar Black','Noto Sans Myanmar Bold','Noto Sans Myanmar ExtraBold','Noto Sans Myanmar ExtraLight','Noto Sans Myanmar Light','Noto Sans Myanmar Medium','Noto Sans Myanmar SemiBold','Noto Sans Myanmar Thin','Noto Sans NKo','Noto Sans Oriya','Noto Sans Oriya Bold','Noto Sans Syriac','Noto Sans Syriac Black','Noto Sans Syriac Bold','Noto Sans Syriac ExtraBold','Noto Sans Syriac ExtraLight','Noto Sans Syriac Light','Noto Sans Syriac Medium','Noto Sans Syriac SemiBold','Noto Sans Syriac Thin','Noto Sans Tagalog', 'Noto Serif Kannada','Noto Serif Kannada Black','Noto Serif Kannada Bold','Noto Serif Kannada ExtraBold','Noto Serif Kannada ExtraLight','Noto Serif Kannada Light','Noto Serif Kannada Medium','Noto Serif Kannada SemiBold','Noto Serif Kannada Thin','Noto Serif Myanmar','Noto Serif Myanmar Black','Noto Serif Myanmar Bold','Noto Serif Myanmar ExtraBold','Noto Serif Myanmar ExtraLight','Noto Serif Myanmar Light','Noto Serif Myanmar Medium','Noto Serif Myanmar SemiBold','Noto Serif Myanmar Thin', 'November Bangla Traditional','November Bangla Traditional Black','November Bangla Traditional Bold','November Bangla Traditional Compressed','November Bangla Traditional Compressed Black','November Bangla Traditional Compressed Bold','November Bangla Traditional Compressed Extralight','November Bangla Traditional Compressed Hairline','November Bangla Traditional Compressed Heavy','November Bangla Traditional Compressed Light','November Bangla Traditional Compressed Medium','November Bangla Traditional Compressed Thin','November Bangla Traditional Condensed','November Bangla Traditional Condensed Black','November Bangla Traditional Condensed Bold','November Bangla Traditional Condensed Extralight','November Bangla Traditional Condensed Hairline','November Bangla Traditional Condensed Heavy','November Bangla Traditional Condensed Light','November Bangla Traditional Condensed Medium','November Bangla Traditional Condensed Thin','November Bangla Traditional Extralight','November Bangla Traditional Hairline','November Bangla Traditional Heavy','November Bangla Traditional Light','November Bangla Traditional Medium','November Bangla Traditional Thin', 'October Compressed Devanagari','October Compressed Devanagari Black','October Compressed Devanagari Bold','October Compressed Devanagari ExtraLight','October Compressed Devanagari Hairline','October Compressed Devanagari Heavy','October Compressed Devanagari Light','October Compressed Devanagari Medium','October Compressed Devanagari Thin','October Compressed Gujarati','October Compressed Gujarati Black','October Compressed Gujarati Bold','October Compressed Gujarati ExtraLight','October Compressed Gujarati Hairline','October Compressed Gujarati Heavy','October Compressed Gujarati Light','October Compressed Gujarati Medium','October Compressed Gujarati Thin','October Compressed Gurmukhi','October Compressed Gurmukhi Black','October Compressed Gurmukhi Bold','October Compressed Gurmukhi ExtraLight','October Compressed Gurmukhi Hairline','October Compressed Gurmukhi Heavy','October Compressed Gurmukhi Light','October Compressed Gurmukhi Medium','October Compressed Gurmukhi Thin','October Compressed Kannada','October Compressed Kannada Black','October Compressed Kannada Bold','October Compressed Kannada ExtraLight','October Compressed Kannada Hairline','October Compressed Kannada Heavy','October Compressed Kannada Light','October Compressed Kannada Medium','October Compressed Kannada Thin','October Compressed Meetei Mayek','October Compressed Meetei Mayek Black','October Compressed Meetei Mayek Bold','October Compressed Meetei Mayek ExtraLight','October Compressed Meetei Mayek Hairline','October Compressed Meetei Mayek Heavy','October Compressed Meetei Mayek Light','October Compressed Meetei Mayek Medium','October Compressed Meetei Mayek Thin','October Compressed Odia','October Compressed Odia Black','October Compressed Odia Bold','October Compressed Odia ExtraLight','October Compressed Odia Hairline','October Compressed Odia Heavy','October Compressed Odia Light','October Compressed Odia Medium','October Compressed Odia Thin','October Compressed Ol Chiki','October Compressed Ol Chiki Black','October Compressed Ol Chiki Bold','October Compressed Ol Chiki ExtraLight','October Compressed Ol Chiki Hairline','October Compressed Ol Chiki Heavy','October Compressed Ol Chiki Light','October Compressed Ol Chiki Medium','October Compressed Ol Chiki Thin','October Compressed Tamil','October Compressed Tamil Black','October Compressed Tamil Bold','October Compressed Tamil ExtraLight','October Compressed Tamil Hairline','October Compressed Tamil Heavy','October Compressed Tamil Light','October Compressed Tamil Medium','October Compressed Tamil Thin','October Compressed Telugu','October Compressed Telugu Black','October Compressed Telugu Bold','October Compressed Telugu ExtraLight','October Compressed Telugu Hairline','October Compressed Telugu Heavy','October Compressed Telugu Light','October Compressed Telugu Medium','October Compressed Telugu Thin','October Condensed Devanagari','October Condensed Devanagari Black','October Condensed Devanagari Bold','October Condensed Devanagari ExtraLight','October Condensed Devanagari Hairline','October Condensed Devanagari Heavy','October Condensed Devanagari Light','October Condensed Devanagari Medium','October Condensed Devanagari Thin','October Condensed Gujarati','October Condensed Gujarati Black','October Condensed Gujarati Bold','October Condensed Gujarati ExtraLight','October Condensed Gujarati Hairline','October Condensed Gujarati Heavy','October Condensed Gujarati Light','October Condensed Gujarati Medium','October Condensed Gujarati Thin','October Condensed Gurmukhi','October Condensed Gurmukhi Black','October Condensed Gurmukhi Bold','October Condensed Gurmukhi ExtraLight','October Condensed Gurmukhi Hairline','October Condensed Gurmukhi Heavy','October Condensed Gurmukhi Light','October Condensed Gurmukhi Medium','October Condensed Gurmukhi Thin','October Condensed Kannada','October Condensed Kannada Black','October Condensed Kannada Bold','October Condensed Kannada ExtraLight','October Condensed Kannada Hairline','October Condensed Kannada Heavy','October Condensed Kannada Light','October Condensed Kannada Medium','October Condensed Kannada Thin','October Condensed Meetei Mayek','October Condensed Meetei Mayek Black','October Condensed Meetei Mayek Bold','October Condensed Meetei Mayek ExtraLight','October Condensed Meetei Mayek Hairline','October Condensed Meetei Mayek Heavy','October Condensed Meetei Mayek Light','October Condensed Meetei Mayek Medium','October Condensed Meetei Mayek Thin','October Condensed Odia','October Condensed Odia Black','October Condensed Odia Bold','October Condensed Odia ExtraLight','October Condensed Odia Hairline','October Condensed Odia Heavy','October Condensed Odia Light','October Condensed Odia Medium','October Condensed Odia Thin','October Condensed Ol Chiki','October Condensed Ol Chiki Black','October Condensed Ol Chiki Bold','October Condensed Ol Chiki ExtraLight','October Condensed Ol Chiki Hairline','October Condensed Ol Chiki Heavy','October Condensed Ol Chiki Light','October Condensed Ol Chiki Medium','October Condensed Ol Chiki Thin','October Condensed Tamil','October Condensed Tamil Black','October Condensed Tamil Bold','October Condensed Tamil ExtraLight','October Condensed Tamil Hairline','October Condensed Tamil Heavy','October Condensed Tamil Light','October Condensed Tamil Medium','October Condensed Tamil Thin','October Condensed Telugu','October Condensed Telugu Black','October Condensed Telugu Bold','October Condensed Telugu ExtraLight','October Condensed Telugu Hairline','October Condensed Telugu Heavy','October Condensed Telugu Light','October Condensed Telugu Medium','October Condensed Telugu Thin','October Devanagari','October Devanagari Black','October Devanagari Bold','October Devanagari ExtraLight','October Devanagari Hairline','October Devanagari Heavy','October Devanagari Light','October Devanagari Medium','October Devanagari Thin','October Gujarati','October Gujarati Black','October Gujarati Bold','October Gujarati ExtraLight','October Gujarati Hairline','October Gujarati Heavy','October Gujarati Light','October Gujarati Medium','October Gujarati Thin','October Gurmukhi','October Gurmukhi Black','October Gurmukhi Bold','October Gurmukhi ExtraLight','October Gurmukhi Hairline','October Gurmukhi Heavy','October Gurmukhi Light','October Gurmukhi Medium','October Gurmukhi Thin','October Kannada','October Kannada Black','October Kannada Bold','October Kannada ExtraLight','October Kannada Hairline','October Kannada Heavy','October Kannada Light','October Kannada Medium','October Kannada Thin','October Meetei Mayek','October Meetei Mayek Black','October Meetei Mayek Bold','October Meetei Mayek ExtraLight','October Meetei Mayek Hairline','October Meetei Mayek Heavy','October Meetei Mayek Light','October Meetei Mayek Medium','October Meetei Mayek Thin','October Odia','October Odia Black','October Odia Bold','October Odia ExtraLight','October Odia Hairline','October Odia Heavy','October Odia Light','October Odia Medium','October Odia Thin','October Ol Chiki','October Ol Chiki Black','October Ol Chiki Bold','October Ol Chiki ExtraLight','October Ol Chiki Hairline','October Ol Chiki Heavy','October Ol Chiki Light','October Ol Chiki Medium','October Ol Chiki Thin','October Tamil','October Tamil Black','October Tamil Bold','October Tamil ExtraLight','October Tamil Hairline','October Tamil Heavy','October Tamil Light','October Tamil Medium','October Tamil Thin','October Telugu','October Telugu Black','October Telugu Bold','October Telugu ExtraLight','October Telugu Hairline','October Telugu Heavy','October Telugu Light','October Telugu Medium','October Telugu Thin', 'Optima','Optima Bold','Optima Bold Italic','Optima ExtraBlack','Optima Italic', 'Oriya MN','Oriya MN Bold','Oriya Sangam MN','Oriya Sangam MN Bold', 'Osaka','Osaka-Mono', 'PSL Ornanong Pro','PSL Ornanong Pro Bold','PSL Ornanong Pro Bold Italic','PSL Ornanong Pro Demibold','PSL Ornanong Pro Demibold Italic','PSL Ornanong Pro Italic','PSL Ornanong Pro Light','PSL Ornanong Pro Light Italic', 'PT Mono','PT Mono Bold','PT Sans','PT Sans Bold','PT Sans Bold Italic','PT Sans Caption','PT Sans Caption Bold','PT Sans Italic','PT Sans Narrow','PT Sans Narrow Bold','PT Serif','PT Serif Bold','PT Serif Bold Italic','PT Serif Caption','PT Serif Caption Italic','PT Serif Italic', 'Padyakke Expanded One', 'Palatino','Palatino Bold','Palatino Bold Italic','Palatino Italic', 'Papyrus','Papyrus Condensed', 'Party LET', 'PCMyungjo', 'Phosphate Inline','Phosphate Solid', 'PilGi', 'PingFang HK','PingFang HK Light','PingFang HK Medium','PingFang HK Semibold','PingFang HK Thin','PingFang HK Ultralight','PingFang MO','PingFang MO Light','PingFang MO Medium','PingFang MO Semibold','PingFang MO Thin','PingFang MO Ultralight','PingFang SC','PingFang SC Light','PingFang SC Medium','PingFang SC Semibold','PingFang SC Thin','PingFang SC Ultralight','PingFang TC','PingFang TC Light','PingFang TC Medium','PingFang TC Semibold','PingFang TC Thin','PingFang TC Ultralight', 'Plantagenet Cherokee', 'Produkt','Produkt Extralight','Produkt Extralight Italic','Produkt Italic','Produkt Light','Produkt Light Italic','Produkt Medium','Produkt Medium Italic', 'Proxima Nova','Proxima Nova Bold','Proxima Nova Bold It','Proxima Nova Extrabold','Proxima Nova Extrabold It','Proxima Nova It','Proxima Nova Light','Proxima Nova Light It','Proxima Nova Medium','Proxima Nova Medium It','Proxima Nova Semibold','Proxima Nova Semibold It', 'Publico Headline Black','Publico Headline Black Italic','Publico Headline Bold','Publico Headline Bold Italic','Publico Headline Italic','Publico Headline Roman','Publico Text Bold','Publico Text Bold Italic','Publico Text Italic','Publico Text Roman','Publico Text Semibold','Publico Text Semibold Italic', 'Quotes Caps','Quotes Script', 'Raanana','Raanana Bold', 'Rockwell','Rockwell Bold','Rockwell Bold Italic','Rockwell Italic', 'STFangsong', 'STHeiti', 'STIX Two Math','STIX Two Text','STIX Two Text Bold','STIX Two Text Bold Italic','STIX Two Text Italic','STIX Two Text Medium Italic','STIX Two Text Medium','STIX Two Text SemiBold','STIX Two Text SemiBold Italic', 'STKaiti', 'STSong', 'STXihei', 'Sama Devanagari','Sama Devanagari Bold','Sama Devanagari Book','Sama Devanagari ExtraBold','Sama Devanagari Medium','Sama Devanagari SemiBold','Sama Gujarati','Sama Gujarati Bold','Sama Gujarati Book','Sama Gujarati ExtraBold','Sama Gujarati Medium','Sama Gujarati SemiBold','Sama Gurmukhi','Sama Gurmukhi Bold','Sama Gurmukhi Book','Sama Gurmukhi ExtraBold','Sama Gurmukhi Medium','Sama Gurmukhi SemiBold','Sama Kannada','Sama Kannada Bold','Sama Kannada Book','Sama Kannada ExtraBold','Sama Kannada Medium','Sama Kannada SemiBold','Sama Malayalam','Sama Malayalam Bold','Sama Malayalam Book','Sama Malayalam ExtraBold','Sama Malayalam Medium','Sama Malayalam SemiBold','Sama Tamil','Sama Tamil Bold','Sama Tamil Book','Sama Tamil ExtraBold','Sama Tamil Medium','Sama Tamil SemiBold', 'Sana', 'Sarabun','Sarabun Bold','Sarabun Bold Italic','Sarabun ExtraBold','Sarabun ExtraBold Italic','Sarabun ExtraLight','Sarabun ExtraLight Italic','Sarabun Italic','Sarabun Light','Sarabun Light Italic','Sarabun Medium','Sarabun Medium Italic','Sarabun SemiBold','Sarabun SemiBold Italic','Sarabun Thin','Sarabun Thin Italic', 'Sathu', 'Sauber Script', 'Savoye LET', 'Shobhika','Shobhika Bold', 'Shree Devanagari 714','Shree Devanagari 714 Bold','Shree Devanagari 714 Bold Italic','Shree Devanagari 714 Italic', 'SignPainter','SignPainter Semibold', 'Silom', 'SimSong','SimSong Bold', 'Sinhala MN','Sinhala MN Bold','Sinhala Sangam MN','Sinhala Sangam MN Bold', 'Skia','Skia Black','Skia Black Condensed','Skia Black Extended','Skia Bold','Skia Condensed','Skia Extended','Skia Light','Skia Light Condensed','Skia Light Extended', 'Snell Roundhand','Snell Roundhand Black','Snell Roundhand Bold', 'Songti SC','Songti SC Black','Songti SC Bold','Songti SC Light', 'Songti TC','Songti TC Bold','Songti TC Light', 'Spot Mono','Spot Mono Bold','Spot Mono Medium', 'Srisakdi','Srisakdi Bold', 'Sukhumvit Set Bold','Sukhumvit Set Light','Sukhumvit Set Medium','Sukhumvit Set Semi Bold','Sukhumvit Set Text','Sukhumvit Set Thin', 'Symbol', 'Tahoma','Tahoma Bold', 'Tamil MN','Tamil MN Bold','Tamil Sangam MN','Tamil Sangam MN Black','Tamil Sangam MN Bold','Tamil Sangam MN Demibold','Tamil Sangam MN Light','Tamil Sangam MN Medium', 'Telugu MN','Telugu MN Bold','Telugu Sangam MN','Telugu Sangam MN Bold', 'Thonburi','Thonburi Bold','Thonburi Light', 'Times New Roman','Times New Roman Bold','Times New Roman Bold Italic','Times New Roman Italic', 'Tiro Bangla','Tiro Bangla Italic','Tiro Devanagari Hindi','Tiro Devanagari Hindi Italic','Tiro Devanagari Marathi','Tiro Devanagari Marathi Italic','Tiro Devanagari Sanskrit','Tiro Devanagari Sanskrit Italic','Tiro Gurmukhi','Tiro Gurmukhi Italic','Tiro Kannada','Tiro Kannada Italic','Tiro Tamil','Tiro Tamil Italic','Tiro Telugu','Tiro Telugu Italic', 'Toppan Bunkyu Gothic','Toppan Bunkyu Gothic Demibold','Toppan Bunkyu Midashi Gothic Extrabold','Toppan Bunkyu Midashi Mincho Extrabold','Toppan Bunkyu Mincho', 'Trattatello', 'Trebuchet MS','Trebuchet MS Bold','Trebuchet MS Bold Italic','Trebuchet MS Italic', 'Tsukushi A Round Gothic','Tsukushi A Round Gothic Bold','Tsukushi B Round Gothic','Tsukushi B Round Gothic Bold', 'Verdana','Verdana Bold','Verdana Bold Italic','Verdana Italic', 'Waseem','Waseem Light', 'Wawati SC','Wawati TC', 'Webdings', 'Weibei SC Bold','Weibei TC Bold', 'Wingdings','Wingdings 2','Wingdings 3', 'Xingkai SC Bold','Xingkai SC Light','Xingkai TC Bold','Xingkai TC Light', 'Yuanti SC','Yuanti SC Bold','Yuanti SC Light','Yuanti TC','Yuanti TC Bold','Yuanti TC Light', 'YuGothic Bold','YuGothic Medium', 'YuKyokasho Bold','YuKyokasho Medium','YuKyokasho Yoko Bold','YuKyokasho Yoko Medium', 'YuMincho +36p Kana Demibold','YuMincho +36p Kana Extrabold','YuMincho +36p Kana Medium','YuMincho Demibold','YuMincho Extrabold','YuMincho Medium', 'Yuppy SC','Yuppy TC', 'Zapf Dingbats', 'Zapfino', ] }, } run_once() </script> </body> </html> ================================================ FILE: tests/fontsystem.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>system fonts</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 380px; max-width: 480px;} #tb12 td {padding-right: 10px;} </style> </head> <body> <div class="offscreen"> <div id="sysFont">hello world</div> </div> <div class="hidden"> <!--widget fonts--> <input type="reset" id="wgtbutton"> <input type="checkbox" id="wgtcheckbox"> <input type="color" id="wgtcolor"> <input type="date" id="wgtdate"> <input type="datetime-local" id="wgtdatetime-local"> <input type="email" id="wgtemail"> <input type="file" id="wgtfile"> <input type="hidden" id="wgthidden"> <input type="image" id="wgtimage"> <input type="month" id="wgtmonth"> <input type="number" id="wgtnumber"> <input type="password" id="wgtpassword"> <input type="radio" id="wgtradio"> <input type="range" id="wgtrange"> <input type="reset" id="wgtreset"> <input type="search" id="wgtsearch"> <select id="wgtselect"><option></option></select> <input type="submit" id="wgtsubmit"> <input type="tel" id="wgttel"> <input type="text" id="wgttext"> <textarea id="wgttextarea"></textarea> <input type="time" id="wgttime"> <input type="url" id="wgturl"> <input type="week" id="wgtweek"> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <col width="1%"><col width="99%"> <thead><tr><th colspan="2"> <div class="nav-title">system fonts <div class="nav-up"><span class="perf" id="language"></span></div> </div> </th></tr></thead> <tr><td></td><td class="mono spaces" id="default"></td></tr> <tr><td colspan="2"></td></tr> <tr><td></td><td class="mono spaces" id="moz"></td></tr> <tr><td colspan="2"></td></tr> <tr><td></td><td class="mono spaces" id="system"></td></tr> <tr><td colspan="2"></td></tr> <tr><td></td><td class="mono spaces" id="widgets"></td></tr> <tr><td colspan="2"></td></tr> </table> <br> <table id="tb12"> <col width="40%"><col width="60%"> <thead><tr><th colspan="2"> <div class="nav-title">check getComputedStyle</div> </th></tr></thead> <!-- always get span width, div height --> <tr> <td><div id="ctrlh0" style="font:caption"><span id="ctrlw0">caption</span></div></td> <td><div id="testh0"><span id="testw0">caption</span></div></td> </tr> <tr><td id="cmeasure0"></td><td id="tmeasure0"></td></tr> <tr><td id="hash0"></td><td id="data0"></td></tr> <tr> <td><div id="ctrlh1" style="font:icon"><span id="ctrlw1">icon</span></div></td> <td><div id="testh1"><span id="testw1">icon</span></div></td> </tr> <tr><td id="cmeasure1"></td><td id="tmeasure1"></td></tr> <tr><td id="hash1"></td><td id="data1"></td></tr> <tr> <td><div id="ctrlh2" style="font:menu"><span id="ctrlw2">menu</span></div></td> <td><div id="testh2"><span id="testw2">menu</span></div></td> </tr> <tr><td id="cmeasure2"></td><td id="tmeasure2"></td></tr> <tr><td id="hash2"></td><td id="data2"></td></tr> <tr> <td><div id="ctrlh3" style="font:message-box"><span id="ctrlw3">message-box</span></div></td> <td><div id="testh3"><span id="testw3">message-box</span></div></td> </tr> <tr><td id="cmeasure3"></td><td id="tmeasure3"></td></tr> <tr><td id="hash3"></td><td id="data3"></td></tr> <tr> <td><div id="ctrlh4" style="font:small-caption"><span id="ctrlw4">small-caption</span></div></td> <td><div id="testh4"><span id="testw4">small-caption</span></div></td> </tr> <tr><td id="cmeasure4"></td><td id="tmeasure4"></td></tr> <tr><td id="hash4"></td><td id="data4"></td></tr> <tr> <td><div id="ctrlh5" style="font:status-bar"><span id="ctrlw5">status-bar</span></div></td> <td><div id="testh5"><span id="testw5">status-bar</span></div></td> </tr> <tr><td id="cmeasure5"></td><td id="tmeasure5"></td></tr> <tr><td id="hash5"></td><td id="data5"></td></tr> <!-- moz --> <tr><td colspan="2" class="center"><br>--- moz ---</td></tr> <tr> <td><div id="ctrlh16" style="font:-moz-bullet-font"><span id="ctrlw16">-moz-bullet-font</span></div></td> <td><div id="testh16"><span id="testw16">-moz-bullet-font</span></div></td> </tr> <tr><td id="cmeasure16"></td><td id="tmeasure16"></td></tr> <tr><td id="hash16"></td><td id="data16"></td></tr> <tr> <td><div id="ctrlh13" style="font:-moz-button"><span id="ctrlw13">-moz-button</span></div></td> <td><div id="testh13"><span id="testw13">-moz-button</span></div></td> </tr> <tr><td id="cmeasure13"></td><td id="tmeasure13"></td></tr> <tr><td id="hash13"></td><td id="data13"></td></tr> <tr> <td><div id="ctrlh18" style="font:-moz-button-group"><span id="ctrlw18">-moz-button-group</span></div></td> <td><div id="testh18"><span id="testw18">-moz-button-group</span></div></td> </tr> <tr><td id="cmeasure18"></td><td id="tmeasure18"></td></tr> <tr><td id="hash18"></td><td id="data18"></td></tr> <tr> <td><div id="ctrlh6" style="font:-moz-desktop"><span id="ctrlw6">-moz-desktop</span></div></td> <td><div id="testh6"><span id="testw6">-moz-desktop</span></div></td> </tr> <tr><td id="cmeasure6"></td><td id="tmeasure6"></td></tr> <tr><td id="hash6"></td><td id="data6"></td></tr> <tr> <td><div id="ctrlh12" style="font:-moz-dialog"><span id="ctrlw12">-moz-dialog</span></div></td> <td><div id="testh12"><span id="testw12">-moz-dialog</span></div></td> </tr> <tr><td id="cmeasure12"></td><td id="tmeasure12"></td></tr> <tr><td id="hash12"></td><td id="data12"></td></tr> <tr> <td><div id="ctrlh7" style="font:-moz-document"><span id="ctrlw7">-moz-document</span></div></td> <td><div id="testh7"><span id="testw7">-moz-document</span></div></td> </tr> <tr><td id="cmeasure7"></td><td id="tmeasure7"></td></tr> <tr><td id="hash7"></td><td id="data7"></td></tr> <tr> <td><div id="ctrlh14" style="font:-moz-field"><span id="ctrlw14">-moz-field</span></div></td> <td><div id="testh14"><span id="testw14">-moz-field</span></div></td> </tr> <tr><td id="cmeasure14"></td><td id="tmeasure14"></td></tr> <tr><td id="hash14"></td><td id="data14"></td></tr> <tr> <td><div id="ctrlh8" style="font:-moz-info"><span id="ctrlw8">-moz-info</span></div></td> <td><div id="testh8"><span id="testw8">-moz-info</span></div></td> </tr> <tr><td id="cmeasure8"></td><td id="tmeasure8"></td></tr> <tr><td id="hash8"></td><td id="data8"></td></tr> <tr> <td><div id="ctrlh15" style="font:-moz-list"><span id="ctrlw15">-moz-list</span></div></td> <td><div id="testh15"><span id="testw15">-moz-list</span></div></td> </tr> <tr><td id="cmeasure15"></td><td id="tmeasure15"></td></tr> <tr><td id="hash15"></td><td id="data15"></td></tr> <tr> <td><div id="ctrlh17" style="font:-moz-message-bar"><span id="ctrlw17">-moz-message-bar</span></div></td> <td><div id="testh17"><span id="testw17">-moz-message-bar</span></div></td> </tr> <tr><td id="cmeasure17"></td><td id="tmeasure17"></td></tr> <tr><td id="hash17"></td><td id="data17"></td></tr> <tr> <td><div id="ctrlh9" style="font:-moz-pull-down-menu"><span id="ctrlw9">-moz-pull-down-menu</span></div></td> <td><div id="testh9"><span id="testw9">-moz-pull-down-menu</span></div></td> </tr> <tr><td id="cmeasure9"></td><td id="tmeasure9"></td></tr> <tr><td id="hash9"></td><td id="data9"></td></tr> <tr> <td><div id="ctrlh10" style="font:-moz-window"><span id="ctrlw10">-moz-window</span></div></td> <td><div id="testh10"><span id="testw10">-moz-window</span></div></td> </tr> <tr><td id="cmeasure10"></td><td id="tmeasure10"></td></tr> <tr><td id="hash10"></td><td id="data10"></td></tr> <tr> <td><div id="ctrlh11" style="font:-moz-workspace"><span id="ctrlw11">-moz-workspace</span></div></td> <td><div id="testh11"><span id="testw11">-moz-workspace</span></div></td> </tr> <tr><td id="cmeasure11"></td><td id="tmeasure11"></td></tr> <tr><td id="hash11"></td><td id="data11"></td></tr> <tr><td colspan="2" class="center"><br>--- system-ui sample text ---</td></tr> <tr><td colspan="2" style="text-align: left;"> <div class="no_color index" style="font-family:system-ui">Vestibulum ante ipsum primis in faucibus orci tor browsera ultrima sposuere onionii curae; Nam maximus thorin erat et ante digitus printus, ac pier justo tincidunt.</div> </td></tr> </table> <br> <script> 'use strict'; let oDefault = {}, oData = {} function check_computed() { for (let i=0; i < 19; i++) { let ctrlW = document.getElementById("ctrlw"+i), ctrlH = document.getElementById("ctrlh"+i), testW = document.getElementById("testw"+i), testH = document.getElementById("testh"+i), hash = document.getElementById("hash"+i), data = document.getElementById("data"+i) try { let propList = ['font-family', 'font-size', 'font-style', 'font-weight',] let aData = [] for (const prop of propList) { let value = getComputedStyle(ctrlW)[prop] testW.style[prop] = value aData.push(value) } // always get span width, div height // can't get div height to always match, so go with span height: seems to work // also can be slightly out in decimal precision, e.g. zoomed // lets try toFixed(4) // measure control let cSpan = ctrlW.getBoundingClientRect() let cDiv = ctrlH.getBoundingClientRect() let cMeasure = (cSpan.width).toFixed(4) +" x "+ (cSpan.height).toFixed(4) document.getElementById("cmeasure"+i).innerHTML = cMeasure // measure test let tSpan = testW.getBoundingClientRect() let tDiv = testH.getBoundingClientRect() let tMeasure = (tSpan.width).toFixed(4) +" x "+ (tSpan.height).toFixed(4) document.getElementById("tmeasure"+i).innerHTML = tMeasure // and compare +" "+ (cMeasure == tMeasure ? green_tick : red_cross) // data hash.innerHTML = s14 + mini(aData.join()) + sc data.innerHTML = aData.join(", ") } catch(e) { console.log(e.name, e.message) } } } function get_moz() { const METRIC = "moz" try { // 1802957: FF109+: -moz no longer applied but keep for regression testing // add bogus '-default-font' to check they are falling back to actual default let aList = [ '-default-font','-moz-bullet-font','-moz-button','-moz-button-group','-moz-desktop','-moz-dialog','-moz-document', '-moz-field','-moz-info','-moz-list','-moz-message-bar','-moz-pull-down-menu','-moz-window','-moz-workspace', ] let aProps = ['font-size','font-style','font-weight','font-family'] let oRes = {} let el = dom.sysFont aList.forEach(function(name){ let aKeys = [] el.style.font = "" // always clear in case a font is invalid/deprecated el.style.font = name for (const k of aProps) {aKeys.push(getComputedStyle(el)[k])} let key = aKeys.join(" ") if (oRes[key] == undefined) {oRes[key] = [name]} else {oRes[key].push(name)} // defaults try { let props = getDefaultComputedStyle(el) let tmphash = mini(props) if (oDefault[tmphash] == undefined) { oDefault[tmphash] = {"hash": tmphash, "items": [name], "metrics": props} } else { oDefault[tmphash].items.push(name) } } catch(e) {} }) // sort let newobj = {} for (const k of Object.keys(oRes).sort()) {newobj[k] = oRes[k]} let hash = mini(newobj) // display dom.moz.innerHTML = s12 +"MOZ: "+ sc +"<br>"+ s14 + hash + sc +"<br>" + json_highlight(newobj) return } catch(e) { dom.moz.innerHTML = s12 +"MOZ: "+ sc + e.name +": " + e.message return } } function get_system() { const METRIC = "system" try { // 1802957: FF109+: -moz no longer applied but keep for regression testing // add bogus '-default-font' to check they are falling back to actual default let aList = ['caption','icon','menu','message-box','small-caption','status-bar'] let aProps = ['font-size','font-style','font-weight','font-family'] let oRes = {} let el = dom.sysFont aList.forEach(function(name){ let aKeys = [] el.style.font = "" // always clear in case a font is invalid/deprecated el.style.font = name for (const k of aProps) {aKeys.push(getComputedStyle(el)[k])} let key = aKeys.join(" ") if (oRes[key] == undefined) {oRes[key] = [name]} else {oRes[key].push(name)} // defaults try { let props = getDefaultComputedStyle(el) let tmphash = mini(props) if (oDefault[tmphash] == undefined) { oDefault[tmphash] = {"hash": tmphash, "items": [name], "metrics": props} } else { oDefault[tmphash].items.push(name) } } catch(e) {} }) // sort let newobj = {} for (const k of Object.keys(oRes).sort()) {newobj[k] = oRes[k]} let hash = mini(newobj) // display dom.system.innerHTML = s12 +"SYSTEM: "+ sc +"<br>"+ s14 + hash + sc +"<br>" + json_highlight(newobj) return } catch(e) { dom.system.innerHTML = s12 +"SYSTEM: "+ sc + e.name +": " + e.message return } } function get_widget() { const METRIC = "widget" try { let aList = [ 'button','checkbox','color','date','datetime-local','email','file','hidden','image','month', 'number','password','radio','range','reset','search','select','submit','tel','text','textarea','time','url','week', ] let oRes = {} aList.forEach(function(name) { let el = dom["wgt"+ name] let key = getComputedStyle(el).getPropertyValue("font-family") +" "+ getComputedStyle(el).getPropertyValue("font-size") if (oRes[key] == undefined) {oRes[key] = [name]} else {oRes[key].push(name)} // defaults try { let props = getDefaultComputedStyle(el) let tmphash = mini(props) if (oDefault[tmphash] == undefined) { oDefault[tmphash] = {"hash": tmphash, "items": [name], "metrics": props} } else { oDefault[tmphash].items.push(name) } } catch(e) {} }) // sort let newobj = {} for (const k of Object.keys(oRes).sort()) {newobj[k] = oRes[k]} let hash = mini(newobj) // display dom.widgets.innerHTML = s12 +"WIDGETS: "+ sc +"<br>"+ s14 + hash + sc +"<br>" + json_highlight(newobj) return } catch(e) { dom.widgets.innerHTML = s12 +"WIDGETS: "+ sc + e.name +": " + e.message return } } try { dom.language.innerHTML = navigator.language } catch(e) { console.log(e) } function logConsole() { console.log(oData) } Promise.all([ get_moz(), get_system(), get_widget(), ]).then(function(){ const METRIC = "getDefaultComputedStyle" if (Object.keys(oDefault).length) { let tmpObj = {} for (const k of Object.keys(oDefault).sort()) { let group = oDefault[k].items.sort() oData[k] = {"group": group, "metrics": oDefault[k]["metrics"]} tmpObj[k] = group.join(", ") } let defhash = mini(oData) let btn = " <span class='btn12 btnc' onclick='logConsole()'>[details]</span>" dom.default.innerHTML = s12 + METRIC +": "+ sc +"<br><br>"+ s14 + defhash + sc + btn +"<br>" + json_highlight(tmpObj) } else { dom.default.innerHTML = s12 + METRIC +": "+ sc +"<br>n/a" } check_computed() }) </script> </body> </html> ================================================ FILE: tests/fontview.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>script view</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} optgroup {font-style:normal;} .viewchars { direction:ltr; unicode-bidi:bidi-override; font-size: 16px; } .btn-right {float: right; position: relative; right: -9px; top: 3px; text-align: right;} .btn-left {float: left; position: relative; left: -9px; top: 3px;} .type-right {float: right; position: relative; right: 0px; top: -12px} /* make sure element is super wide so text is always on one line */ .measure { position: absolute; left: -5000px; top: 0px; } </style> </head> <body> <div id="dMeasure" class="measure"><span id="sMeasure"></span></div> <div id="fallback" class="offscreen"></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#fonts">return to TZP index</a></td></tr> </table> <table id="tb12"> <thead><tr><th colspan="2"> <div class="nav-title">script/language view <div class="nav-up"><span class="perf" id="lang"></span></div> </div> </th></tr></thead> <tr> <td style="text-align: center; vertical-align: bottom;"> <form> <!--<label for="scripts"> &nbsp; script:</label>--> <select name="scripts" id="scripts" onChange="run(`scripts`)" onClick="run(`scripts`)"><option></option></select> <span class="btn btn12 btn-left" onClick="cycle('back','scripts')">[ &nbsp; PREV &nbsp; ]</span> <span class="btn btn12 btn-right" onClick="cycle('forward','scripts')">[ &nbsp; NEXT &nbsp; ]</span> </form><br> <form> <!--<label for="scripts">language:</label>--> <select name="languages" id="languages" onChange="run(`languages`)" onClick="run(`languages`)"><option></option></select> <span class="btn btn12 btn-left" onClick="cycle('back','languages')">[ &nbsp; PREV &nbsp; ]</span> <span class="btn btn12 btn-right" onClick="cycle('forward','languages')">[ &nbsp; NEXT &nbsp; ]</span> </form><br> <span class="no_color mono"> <code>arrow</code> keys: <code>up</code>/<code>down</code> switch type | <code>left</code> prev | <code>right</code> next </span> </td> </tr> <tr><td><hr></td></tr> <tr> <td> <div class="s14 mono" align="left" id="divInfo"></div> <div class="s14 type-right mono spaces" id="divType"></div> <br> <div class="no_color" align="left"> <div class="s12 mono spaces">monospace <span class="no_color" id="cmono"></span></div><br> <div class="spaces monospace viewchars" id="divMono">hello world</div><br> <div class="s12 mono spaces">sans-serif <span class="no_color" id="csans"></span></div><br> <div class="spaces sans-serif viewchars" id="divSans">hello world</div><br> <div class="s12 mono spaces">serif <span class="no_color" id="cseri"></span></div><br> <div class="spaces serif viewchars" id="divSerif">hello world</div><br> </div> </td></tr> </table> <br> <script> 'use strict'; // https://en.wikipedia.org/wiki/List_of_Unicode_characters // https://www.cogsci.ed.ac.uk/~richard/unicode-sample-3-2.html let oSentence = { // using: https://github.com/mozilla-l10n/focusios-l10n // TB supported 'ar (arabic)': 'امسح تأريخ جلسة التصفح كله و كلمات السر و الكعكات في أي وقت بنقرة واحدة.', 'be (belarusian)': 'Сцірайце ўсю гісторыю аглядання, паролі і кукі калі захочаце ў адзін націск.', 'bg (bulgarian)': 'Изчистване по всяко време с едно докосване на цялата сесия на разглеждане: история, пароли и бисквитки.', 'ca (catalan)': 'Esborreu l\'historial de navegació, les contrasenyes i les galetes quan vulgueu amb un sol toc.', 'cs (czech)': 'Vyčistěte kdykoli celou vaši historii prohlížení, hesla a soubory cookie jedním klepnutím.', 'da (danish)': 'Du kan når som helst slette hele din browserhistorik, dine gemte adgangskoder og dine cookies med et enkelt tryk.', 'de (german)': 'Löschen Sie mit einer einzigen Berührung die gesamte Chronik, Passwörter und Cookies Ihrer Sitzung.', 'el (greek)': 'Απαλοιφή ιστορικού περιήγησης, κωδικών πρόσβασης, cookies ανά πάσα στιγμή, με ένα πάτημα.', 'en (english)': 'Clear your entire browsing session history, passwords, cookies anytime with a single tap.', 'es-ES (spanish spain)': 'Limpia todo el historial de navegación de tu sesión, contraseñas y cookies en cualquier momento con tan solo un clic.', 'fa (persian)': 'تمام تاریخچه،‌کلمه عبور،‌و کوکی های جلسه مرورگر خود را در هر زمانی تنها با یک لمس پاک کنید.', 'fi (finnish)': 'Tyhjennä koko selaushistoriasi, salasanat ja evästeet milloin tahansa yhdellä napautuksella.', 'fr (french)': 'Supprimez votre historique de navigation, vos mots de passe, vos cookies et plus encore, d’une simple pression.', 'ga (irish)': 'Scrios fianáin chomh maith le do stair chuardaigh agus stair bhrabhsála.', // different sentence 'he (hebrew)': 'ניתן בכל עת לנקות את כל היסטורית הגלישה שלך, הסיסמאות והעוגיות בהקשה אחת.', 'hu (hungarian)': 'Törölje az összes böngészési előzményét, jelszavát, és sütijét bármikor, egyetlen koppintással.', 'id (indonesian)': 'Bersihkan semua riwayat sesi penjelajahan, sandi, dan kuki Anda kapan pun dalam sekali ketuk saja.', 'is (icelandic)': 'Hreinsa feril, lykilorð, vefkökur hvenær sem er með einni snertingu.', 'it (italian)': 'Cancella l’intera cronologia di navigazione, le password e i cookie con un semplice tocco, in qualunque momento.', 'ja (japanese)': 'ブラウジングセッション履歴やパスワード、Cookie を、いつでもワンタップですべて消去できます。', 'ka (georgian)': 'გაასუფთავეთ მონახულებული გვერდები, პაროლები და ა.შ. ნებისმიერ დროს, მხოლოდ ერთი შეხებით.', 'ko (korean)': '브라우징 세션 기록이나 비밀번호, 쿠키를 언제나 탭 한번으로 삭제합니다.', 'lt (lithuanian)': 'Bet kuriuo metu išvalykite visą savo naršymo žurnalą, slaptažodžius ir slapukus vienu bakstelėjimu.', 'mk (macedonian)': 'Сакам да разменам пари/натнички чекови.', // I want to exchange some money/travellers cheques. 'ms (malay)': 'Buang seluruh sejarah pelayaran, kata laluan dan kuki dengan sekali tekan sahaja.', 'my (burmese)': 'ရှာဖွေမှုမှတ်တမ်း၊ စကားဝှက်များနှင့် အခြား စသည်တို့ကို တစ်ခါတို့ထိရုံဖြင့် ဖျက်နိုင်သည်။', 'nl (dutch)': 'Wis uw volledige surfgeschiedenis, wachtwoorden en cookies wanneer u wilt met één tik.', 'nn (norwegian nynorsk)': 'Slett hele nettleserhistorikken din, passord, infokapsler når som helst med ett trykk.', // tb is actually nb-no = norwegian bokmål 'pl (polish)': 'Usuwaj całą historię przeglądania, hasła i ciasteczka jednym stuknięciem.', 'pt-BR (portuguese brazil)': 'Limpe todo o histórico, senhas e cookies da sessão de navegação quando quiser, com um simples toque.', 'pt-PT (portuguese portual)': 'Limpe o seu histórico de sessão de navegação inteiro, palavras-passe, cookies a qualquer altura com um simples toque.', 'ro (romanian)': 'Șterge întregul istoric al sesiunii, parolele și cookie-urile oricând, cu o singură atingere.', 'ru (russian)': 'В любой момент стирайте одним нажатием всю историю просмотров, пароли и куки.', 'sq (albanian)': 'Fshini kurdo, me një prekje të vetme, historikun e shfletimit për sesionin, fjalëkalimet, cookie-t.', 'sv (swedish)': 'Rensa hela din webbhistorik, lösenord, kakor när som helst med ett enda tryck.', 'th (thai)': 'ล้างประวัติ, รหัสผ่าน และคุกกี้ในวาระการท่องเว็บทั้งหมดของคุณได้ทุกเวลาด้วยการแตะครั้งเดียว', 'tr (turkish)': 'İstediğiniz zaman tek bir dokunuşla gezinti geçmişinizi, parolaları ve çerezleri temizleyebilirsiniz.', 'uk (ukrainian)': 'Будь-коли стирайте всю історію перегляду, паролі та куки одним дотиком.', 'vi (vietnamese)': 'Xóa toàn bộ lịch sử duyệt web, mật khẩu, cookie bất kỳ lúc nào chỉ với một cú chạm.', 'zh-cn (chinese china)': '随时一键清除您的所有浏览记录、密码及 Cookie。', 'zh-tw (chinese taiwan)': '只要輕鬆一點,就能清除這次上網的所有瀏覽紀錄、儲存密碼、Cookie 等資料。', // other 'am (amharic)': 'በአንድ ጊዜ መታ በማድረግ ሁሉንም የአሰሳ ክፍለ ጊዜ ታሪክዎን ፣ የይለፍ ቃሎችን ፣ ኩኪዎችን በማንኛውም ጊዜ ያጽዱ።', 'an (aragonese)': 'Borra l\'historial completo d\'a sesión de navegación, claus, cookies, en qualsequier momento con un simple toque.', 'ast (asturian)': 'Llimpia cuando quieras y con un calcu les cookies, l\'historial de restolar y les contraseñes.', 'az (azerbaijani)': 'Tək bir toxunuşla istədiyiniz vaxt səyahət sessiyanızın tarixçəsini, parolları və çərəzləri təmizləyin.', 'bn (bengali)': 'আপনার সম্পূর্ণ ব্রাউজিং সেশনের ইতিহাস, পাসওয়ার্ডগুলি, কুকিকে একবার ট্যাপ করে যে কোনো সময় সাফ করুন।', 'bs (bosnian)': 'Izbrišite svoju historiju pretraživanja, lozinke i kolačiće bilo kada s jednim dodirom.', 'co (corsican)': 'Squassate a vostra cronolugia di navigazione, e vostre parolle d’intesa, i vostri canistrelli, à ogni mumentu, cù un’incalcata simplice.', 'cy (welsh)': 'Cliriwch hanes pori, cyfrineiriau, cwcis eich sesiwn unrhyw bryd gydag un cyffyrddiad.', 'dsb (lower sorbian)': 'Wulašujśo swóju póseźeńsku historiju, swóje gronidła a cookieje kuždy cas z jadnym dotyknjenim.', 'eo (esperanto)': 'Iam ajn, per nura tuŝeto, viŝi vian tutan retuman seancan historion, pasvortojn, kuketojn.', 'et (estonian)': 'Kustuta ühe puudutusega kogu lehitsemise seansi ajalugu, paroolid ja küpsised.', 'eu (basque)': 'Sakatze hutsarekin, garbitu edozein unetan zure nabigatze-historia osoa, pasahitzak eta cookieak.', 'gd (scottish gaelic)': 'Falamhaich eachdraidh an t-seisein bhrabhsaidh, na faclan-faire agus briosgaidean agad uair sam bith le aon ghnogag.', 'gl (galician)': 'Borre todo o seu historial de sesións de navegación, contrasinais e cookies en calquera momento cun só toque.', 'gu (gujarati)': 'કંઈક અલગ શોધી રહ્યાં છો? કોઈ અલગ શોધ એન્જિન પસંદ કરો. ગોપનીયતા સૂચના.', // mix of two different sentence 'hi (hindi)': 'अपने ब्राउज़िंग सत्र का इतिहास, कूटशब्दो, और स्मृतिशेषों को किसी भी समय एक टैप में मिटाएँ.', 'hsb (upper sorbian)': 'Zhašejće swoju posedźensku historiju, swoje hesła a placki kóždy čas z jednym podótkom.', 'hy (armenian)': 'Մաքրում է դիտարկումների պատմությունը, գաղտնաբառերը և ավելին՝ մեկ սեղմամբ:', 'ia (interlingua)': 'Vacua tote le chronologia, contrasignos e cookies de tu session de navigation, quando tu vole, con un sol tocco.', 'kab (kabyle)': 'Kkes amazray inek tiγimit n tuinigin, awalen inek uffiren d wayen nniḍen d ayen fessusen s waṭas.', 'kk (kazakh)': 'Шолулар тарихын, парольдерді және cookies файлдарын кез келген уақытта бір шертумен өшіріңіз.', 'km (khmer)': 'មាត្រា ១ មនុស្សទាំងអស់ កើតមកមានសេរីភាព និងសមភាព ក្នុងផ្នែកសេចក្ដីថ្លៃថ្នូរនិងសិទ្ធិ។ មនុស្ស មានវិចារណញ្ញាណនិងសតិសម្បជញ្ញៈជាប់ពីកំណើត ហើយគប្បីប្រព្រឹត្ដចំពោះគ្នាទៅវិញទៅមក ក្នុង ស្មារតីភាតរភាពជាបងប្អូន។', // Article 1 of the Universal Declaration of Human Rights // ^ All human beings are born free and equal in dignity and rights. // ^ They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood. 'kn (kannada)': 'ನಿಮ್ಮ ಸಂಪೂರ್ಣ ಬ್ರೌಸಿಂಗ್ ಸೆಷನ್ ಇತಿಹಾಸ, ಪಾಸ್ವರ್ಡ್ಗಳು, ಕುಕೀಸ್ ಅನ್ನು ಒಮ್ಮೆ ತಟ್ಟುವುದರಿಂದ ಯಾವುದೇ ಸಮಯದಲ್ಲಿ ತೆರವುಗೊಳಿಸಿ.', 'lo (lao)': 'ລືບປະຫວັດເຊສຊັນການທ່ອງເວັບ, ລະຫັດຜ່ານ ແລະ ອື່ນໆທັງຫມົດຂອງທ່ານໂດຍການແຕະບາດດຽວ.', 'lv (latvian)': 'Tas ļauj jums uzņemt un augšupielādēt fotoattēlus. Vietnes, ko apmeklējat, var pieprasīt jūsu atrašanās vietu.', // mix of two different sentence 'mr (marathi)': 'आपला संपूर्ण ब्राऊझिंग इतिहास, पासवर्ड आणि कुकीज केव्हाही एका टॅप मध्ये नष्ट करा.', 'ne (nepali)': 'एक क्लिकमा नै तपाईंको सम्पुर्ण ब्राउजिङ्ग इतिहास, पासवर्डहरु, कुकिजहरु कुनैपनि समयमा खाली हुनेछ ।', 'pa (punjabi)': 'ਆਪਣੇ ਪੂਰੇ ਬਰਾਊਜ਼ਿੰਗ ਸ਼ੈਸ਼ਨ ਅਤੀਤ, ਪਾਸਵਰਡ, ਕੂਕੀਜ਼ ਨੂੰ ਕਿਸੇ ਵੀ ਵੇਲੇ ਇੱਕ ਛੋਹ ਨਾਲ ਮਿਟਾ ਦਿਓ।', 'ses (koyraboro senni)': 'Woo ga naŋ war ma biyey zaa k\'i zijandi. Interneti nungey kaŋ war g\'i naaru ga hin ka war gorodogoo hãa.', // mix of two different sentence 'si (sinhala)': 'ඔබගේ සමස්ත පිරික්සුම් වාර ඉතිහාසය, මුරපද, දත්තකඩ ඕනෑම විටෙක තනි තට්ටු කිරීමකින් මකන්න.', 'sk (slovak)': 'Kedykoľvek si vyčistite celú históriu prehliadania, heslá a cookies. Stačí vám na to jedno ťuknutie.', 'sl (slovenian)': 'Kadarkoli izbrišite celotno zgodovino brskanja, gesla in piškotke z enim dotikom.', 'ta (tamil)': 'உங்கள் ஒட்டுமொத்த உலாவல் அமர்வு வரலாறு, கடவுச்சொற்கள், நினைவிகளை எந்நேரத்திலும் ஒரே தட்டில் துடைக்கவும்.', 'te (telugu)': 'ఎప్పుడైనా కేవలం ఒకే నొక్కుతో మీ విహరణ సెషను చరిత్ర అంతా, సంకేతపదాలు, కుకీలు తుడిచివేసుకోండి.', 'tl (tagalog)': 'I-clear ang iyong buong kasaysayan ng session sa pagba-browse, mga password, cookies anumang oras na may isang solong tapikin.', 'tt (tatar)': 'Бер басу аша бөтен гизү тарихын, серсүзләрне һәм кукиларны теләсә кайчан бетерә аласыз.', 'uz (uzbek)': 'Istalgan vaqtida bir ishora bilan brauzer tarixi, parol va cookie fayllarni tozalab tashlang.', } let oBlocks = { /* skipped unicode 16.0: https://www.unicode.org/charts/PDF/Unicode-16.0/ todhri: https://en.wikipedia.org/wiki/Todhri_(Unicode_block) garay: https://en.wikipedia.org/wiki/Garay_(Unicode_block) / african unicode 17.0: https://www.unicode.org/charts/PDF/Unicode-17.0/ */ // AFRICAN 'african' : { 'adlam': {link: 'Adlam_(Unicode_block)', prefix: '1E9', blocks: [0,1,2,3,4,5], reserved: ['4C','4D','4E','4F','5A','5B','5C','5D']}, 'bamum': {link: 'Bamum_(Unicode_block)', prefix: 'A6', blocks: ['A','B','C','D','E','F'], trim: 8}, 'bassa vah': {link: 'Bassa_Vah_(Unicode_block)', prefix: '16A', blocks: ['D','E','F'], trim: 10, reserved: ['EE','EF']}, 'ethiopic': {link: 'Ethiopic_(Unicode_block)', prefix: 1, trim: 3, enlarge: 14, //* partial: block 35 covers the last changes in 2010: v3 1999 = 345 | v4.1 2005 = +11 | v6 2010 = +2 blocks: [34,35,36,37], partial: true, //*/ /* full blocks: [20,21,22,23,24,25,26,27,28,29,'2A','2B','2C','2D','2E','2F',30,31,32,33,34,35,36,37], reserved: ['249','24E','24F','257','259','25E','25F','289','28E','28F','2B1','2B6','2B7','2BF','2C1','2C6','2C7','2D7','311','316','317','35B','35C'], //*/ }, 'ethiopic supplement': {link: 'Ethiopic_Supplement', prefix: 13, blocks: [8,9], trim: 6, enlarge: 16}, 'medefaidrin': {link: 'Medefaidrin_(Unicode_block)', prefix: '16E', blocks: [4,5,6,7,8,9], trim: 5}, 'mende_kikakui': {link: 'Mende_Kikakui_(Unicode_block)', prefix: '1E8', //* partial: all v7 2004 blocks: [0,1,2,3], partial: true, //*/ /* full blocks: [0,1,2,3,4,5,6,7,8,6,'A','B','C','D'], trim: 9, reserved: ['C5','C6'] //*/ }, 'nko': {link: 'NKo_(Unicode_block)', prefix: '07', blocks: ['C','D','E','F'], reserved: ['FB','FC']}, 'osmanya': {link: 'Osmanya_(Unicode_block)', prefix: 104, blocks: [8,9,'A'], trim: 6, reserved: ['9E','9F']}, 'tifinagh': {link: 'Tifinagh_(Unicode_block)', prefix: '2D', blocks: [3,4,5,6,7], reserved: ['68','69','6A','6B','6C','6D','6E','71','72','73','74','75','76','77','78','79','7A','7B','7C','7D','7E'], ignored: ['7F'] // tifinagh consonant joiner }, 'vai': {link: 'Vai_(Unicode_block)', prefix: 'A', //* partial: all v5.1 2008 blocks: [50,51,52,53], partial: true, //*/ /* full blocks: [50,51,52,53,54,55,56,57,58,59,'5A','5B','5C','5D','5E','5F',60,61,62,63], trim: 20, //*/ }, }, // AMERICAN 'american': { 'cherokee': {link: 'Cherokee_(Unicode_block)', prefix: 13, blocks: ['A','B','C','D','E','F',], trim: 2, reserved: ['F6','F7']}, 'cherokee supplement': {link: 'Cherokee_Supplement', prefix: 'AB', blocks: [7,8,9,'A','B'], enlarge: 16}, 'deseret': {link: 'Deseret_(Unicode_block)', prefix: '104', blocks: [0,1,2,3,4], enlarge: 14}, 'osage': {link: 'Osage_(Unicode_block)', prefix: 104, blocks: ['B','C','D','E','F'], trim: 4, reserved: ['D4','D5','D6','D7']}, 'unified canadian aboriginal syllabics': {link: 'Unified_Canadian_Aboriginal_Syllabics_(Unicode_block)', prefix: 1, //* partial: v3 1999 630 | v5.2 2009 + 10 (9 of these 10 covered in block 67) blocks: [64,65,66,67], partial: true //*/ /* full blocks: [40,41,42,43,44,45,46,47,48,49,'4A','4B','4C','4D','4E','4F',50,51,52,53,54,55,56,57,58,59,'5A','5B','5C','5D','5E','5F',60,61,62,63,64,65,66,67] //*/ }, }, // ANCIENT 'ancient and historic': { 'gothic': {link: 'Gothic_(Unicode_block)', prefix: 103, blocks: [3,4], trim: 5}, 'ogham': {link: 'Ogham_(Unicode_block)', prefix: 16, blocks: [8,9], trim: 3, enlarge: 18}, 'old italic': {link: 'Old_Italic_(Unicode_block)', prefix: 103, blocks: [0,1,2], reserved: ['24','25','26','27','28','29','2A','2B','2C']}, 'old turkic': {link: 'Old_Turkic_(Unicode_block)', prefix: '10C', blocks: [0,1,2,3,4], trim: 7, enlarge: 14}, 'runic': {link: 'Runic_(Unicode_block)', prefix: 16, blocks:['A','B','C','D','E','F'], trim: 7, enlarge: 14}, }, // BRAHMIC 'brahmic': { 'balinese': {link: 'Balinese_(Unicode_block)', prefix: '1B', blocks: [0,1,2,3,4,5,6,7], reserved: ['4D']}, 'bengali': {link: 'Bengali_(Unicode_block)', prefix: '09', blocks: [8,9,'A','B','C','D','E','F'], trim: 1, reserved: ['84','8D','8E','91','92','A9','B1','B3','B4','B5','BA','BB','C5','C6','C9','CA','CF', 'D0','D1','D2','D3','D4','D5','D6','D8','D9','DA','DB','DE','E4','E5'], enlarge: 14 }, 'buginese': {link: 'Buginese_(Unicode_block)', prefix: '1A', blocks: [0,1], reserved: ['1C','1D'], enlarge: 16}, 'buhid': {link: 'Buhid_(Unicode_block)', prefix: 17, blocks: [4,5], trim: 12, enlarge: 16}, 'chakma': {link: 'Chakma_(Unicode_block)', prefix: 111, blocks: [0,1,2,3,4], trim: 8, reserved: ['35']}, 'cham':{link: 'Cham_(Unicode_block)', prefix: 'AA', blocks: [0,1,2,3,4,5], reserved: ['37','38','39','3A','3B','3C','3D','3E','3F','4E','4F','5A','5B'] }, 'common indic number forms': {link: 'Common_Indic_Number_Forms', prefix: 'A8', blocks: [3], trim: 6}, 'devanagari': {link: 'Devanagari_(Unicode_block)', prefix: '09', blocks: [0,1,2,3,4,5,6,7], enlarge: 14}, 'dives akuru': {link: 'Dives_Akuru_(Unicode_block)', prefix: 119, blocks: [0,1,2,3,4,5], trim: 6, reserved: ['07','08','0A','0B','14','17','36','39','3A','47','48','49','4A','4B','4C','4D','4E','4F'] }, 'dogra': {link: 'Dogra_(Unicode_block)', prefix: 118, blocks: [0,1,2,3,4], trim: 20}, 'gujarati': {link: 'Gujarati_(Unicode_block)', prefix: '0A', blocks: [8,9,'A','B','C','D','E','F'], reserved: ['80','84','8E','92','A9','B1','B4','BA','BB','C6','CA','CE','CF','D1','D2','D3','D4','D5', 'D6','D7','D8','D9','DA','DB','DC','DD','DE','DF','E4','E5','F2','F3','F4','F5','F6','F7','F8'] }, 'gurmukhi': {link: 'Gurmukhi_(Unicode_block)', prefix: '0A', blocks: [0,1,2,3,4,5,6,7], trim: 9, reserved: ['00','04','0B','0C','0D','0E','11','12','29','31','34','37','3A','3B','3D','43','44','45','46', '49','4A','4E','4F','50','52','53','54','55','56','57','58','5D','5F','60','61','62','63','64','65'] }, 'hanifi rohingya': {link: 'Hanifi_Rohingya__(Unicode_block)', prefix: '10D', blocks: [0,1,2,3], trim: 6, reserved: ['28','29','2A','2B','2C','2D','2E','2F'] }, 'hanunoo': {link: 'Hanunoo_(Unicode_block)', prefix: 17, blocks: [2,3], trim: 9}, 'javanese': {link: 'Javanese_(Unicode_block)', prefix: 'A9', blocks: [8,9,'A','B','C','D'], reserved: ['CE','DA','DB','DC','DD']}, 'kaithi': {link: 'Kaithi_(Unicode_block)', prefix: 110, blocks: [8,9,'A','B','C'], trim: 2, reserved: ['C3','C4','C5','C6','C7','C8','C9','CA','CB','CC'] }, 'kannada': {link: 'Kannada_(Unicode_block)', prefix: '0C', blocks: [8,9,'A','B','C','D','E','F'], trim: 12, reserved: ['8D','91','A9','B4','BA','BB','C5','C9','CE','CF','D0','D1','D2','D3','D4','D7','D8','D9','DA','DB','DF','E4','E5','F0'] }, 'kawi': {prefix: '11F', blocks: [0,1,2,3,4,5], trim: 5, reserved: ['11','3B','3C','3D'], link: 'Kawi_(Unicode_block)'}, 'kayah li': {prefix: 'A9', blocks: [0,1,2], link: 'Kayah_Li_(Unicode_block)'}, 'khmer': {link: 'Khmer_(Unicode_block)', prefix: 17, blocks: [8,9,'A','B','C','D','E','F'], trim: 6, reserved: ['DE','DF','EA','EB','EC','ED','EE','EF'], ignored: ['B4','B5'] // ignored: KIV AQ, KIV AA }, 'khmer symbols': {link: 'Khmer_Symbols', prefix: 19, blocks: ['E','F']}, 'khojki': {link: 'Khojki_(Unicode_block)', prefix: 112, blocks: [0,1,2,3,4], trim: 14, reserved: ['12']}, 'khudawadi': {link: 'Khudawadi_(Unicode_block)', prefix: 112, blocks: ['B','C','D','E','F'], trim: 6, reserved: ['EB','EC','ED','EE','EF']}, 'lao': {link: 'Lao_(Unicode_block)', prefix: '0E', blocks: [8,9,'A','B','C','D','E','F'], trim: 32, reserved: ['80','83','85','8B','A4','A6','BE','BF','C5','C7','CF','DA','DB'] }, 'lepcha': {link: 'Lepcha_(Unicode_block)', prefix: '1C', blocks: [0,1,2,3,4], reserved: ['38','39','3A','4A','4B','4C']}, 'limbu': {link: 'Limbu_(Unicode_block)', prefix: 19, blocks: [0,1,2,3,4], reserved: ['1F','2C','2D','2E','2F','3C','3D','3E','3F','41','42','43']}, 'mahajani': {link: 'Mahajani_(Unicode_block)', prefix: 111, blocks: [5,6,7], trim: 9, enlarge: 16}, 'malayalam': {link: 'Malayalam_(Unicode_block)', prefix: '0D', blocks: [0,1,2,3,4,5,6,7], reserved: ['0D','11','45','49','50','51','52','53','64','65'], ignored: ['4E'] // ignored: letter dot reph }, 'meetei mayek': {link: 'Meetei_Mayek_(Unicode_block)', prefix: 'AB', blocks: ['C','D','E','F'], trim: 6, reserved: ['EE','EF']}, 'modi': {link: 'Modi_(Unicode_block)', prefix: 116, blocks: [0,1,2,3,4,5], trim: 6, reserved: ['45','46','47','48','49','4A','4B','4C','4D','4E','4F']}, 'mro': {link: 'Mro_(Unicode_block)', prefix: '16A', blocks: [4,5,6], reserved: ['5F','6A','6B','6C','6D']}, 'multani': {link: 'Multani_(Unicode_block)', prefix: 112, blocks: [8,9,'A'], trim: 6, reserved: ['87','89','8E','9E']}, 'myanmar': {link: 'Myanmar_(Unicode_block)', prefix: 10, blocks: [0,1,2,3,4,5,6,7,8,9]}, 'myanmar extended-a': {link: 'Myanmar_Extended-A', prefix: 'AA', blocks: [6,7]}, 'myanmar extended-b': {link: 'Myanmar_Extended-B', prefix: 'A9', blocks: ['E','F'], trim: 1}, 'nag mundari': {link: 'Nag_Mundari_(Unicode_block)', prefix: '1E4', blocks: ['D','E','F'], trim: 6}, 'new tai lue': {link: 'New_Tai_Lue_(Unicode_block)', prefix: 19, blocks: [8,9,'A','B','C','D'], reserved: ['AC','AD','AE','AF','CA','CB','CC','CD','CE','CF','DB','DC','DD'] }, 'newa': {link: 'Newa_(Unicode_block)', prefix: 114, blocks: [0,1,2,3,4,5,6,7], trim: 30, reserved: ['5C']}, 'ol chiki': {link: 'Ol_Chiki_(Unicode_block)', prefix: '1C', blocks: [5,6,7]}, 'oriya': {link: 'Oriya_(Unicode_block)', prefix: '0B', blocks: [0,1,2,3,4,5,6,7], trim: 8, enlarge: 14, reserved: ['00','04','0D','0E','11','12','29','31','34','3A','3B','45','46','49','4A','4E','4F','50','51', '52','53','54','58','59','5A','5B','5E','64','65'], }, 'pau cin hau': {link: 'Pau_Cin_Hau_(Unicode_block)', prefix: '11A', blocks: ['C','D','E','F'], trim: 7}, 'phags-pa': {link: 'Phags-pa_(Unicode_block)', prefix: 'A8', blocks: [4,5,6,7], trim:-8}, 'saurashtra': {link: 'Saurashtra_(Unicode_block)', prefix: 'A8', blocks: [8,9,'A','B','C','D'], trim: 6, reserved: ['C6','C7','C8','C9','CA','CB','CC','CD'] }, 'sinhala': {link: 'Sinhala_(Unicode_block)', prefix: '0D', blocks: [8,9,'A','B','C','D','E','F'], trim: 11, enlarge: 16, reserved: ['80','84','97','98','99','B2','BC','BE','BF','C7','C8','C9','CB','CC','CD','CE','D5','D7','E0','E1','E2','E3','E4','E5','F0','F1'] }, 'sora sompeng': {link: 'Sora_Sompeng_(Unicode_block)', prefix: 110, blocks: ['D','E','F'], trim: 6, enlarge: 16, reserved: ['E9','EA','EB','EC','ED','EE','EF'] }, 'sundanese': {link: 'Sundanese_(Unicode_block)', prefix: '1B', blocks: [8,9,'A','B'], ignored: ['AB']}, // ignored: sign virama 'sundanese supplement': {link: 'Sundanese_Supplement', prefix: '1C', blocks: ['C'], trim: 8}, 'syloti nagri': {link: 'Syloti_Nagri_(Unicode_block)', prefix: 'A8', blocks: [0,1,2], trim: 3, }, 'tagalog': {link: 'Tagalog_(Unicode_block)', prefix: 17, blocks: [0,1,], reserved: ['16','17','18','19','1A','1B','1C','1D','1E']}, 'tagbanwa': {link: 'Tagbanwa_(Unicode_block)', prefix: 17, blocks: [6,7], trim: 12, reserved: ['6D','71']}, 'tai le': {link: 'Tai_Le_(Unicode_block)', prefix: 19, blocks: [5,6,7], trim: 11, reserved: ['6E','6F'], enlarge: 14}, 'tai tham': {link: 'Tai_Tham_(Unicode_block)', prefix: '1A', blocks: [2,3,4,5,6,7,8,9,'A'], trim: 2, reserved: ['5F','7D','7E','8A','8B','8C','8D','8E','8F','9A','9B','9C','9D','9E','9F'] }, 'tai viet': {link: 'Tai_Viet_(Unicode_block)', prefix: 'AA', blocks: [8,9,'A','B','C','D'], reserved: ['C3','C4','C5','C6','C7','C8','C9','CA','CB','CC','CD','CE','CF','D0','D1','D2','D3','D4','D5','D6','D7','D8','D9','DA'] }, 'takri': {link: 'Takri_(Unicode_block)', prefix: 116, blocks: [8,9,'A','B','C'], trim: 6, reserved: ['BA','BB','BC','BD','BE','BF']}, 'tamil': {link: 'Tamil_(Unicode_block)', prefix: '0B', blocks: [8,9,'A','B','C','D','E','F'], trim: 5, reserved: ['80','81','84','8B','8C','8D','91','96','97','98','9B','9D','A0','A1','A2','A5','A6','A7','AB','AC','AD','BA','BB','BC','BD', 'C3','C4','C5','C9','CE','CF','D1','D2','D3','D4','D5','D6','D8','D9','DA','DB','DC','DD','DE','DF','E0','E1','E2','E3','E4','E5'] }, 'tamil supplement': {link: 'Tamil_Supplement', prefix: '11F', blocks: ['C','D','E','F'], reserved: ['F2','F3','F4','F5','F6','F7','F8','F9','FA','FB','FC','FD','FE'] }, 'telugu': {link: 'Telugu_(Unicode_block)', prefix: '0C', blocks: [0,1,2,3,4,5,6,7], enlarge: 16, reserved: ['0D','11','29','3A','3B','45','49','4E','4F','50','51','52','53','54','57','5B','5E','5F','64','65','70','71','72','73','74','75','76'] }, 'thai': {link: 'Thai_(Unicode_block)', prefix: '0E', blocks: [0,1,2,3,4,5,6,7], trim: 36, enlarge: 14, reserved: ['00','3B','3C','3D','3E'] }, 'tibetan': {link: 'Tibetan_(Unicode_block)', prefix: '0F', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], trim: 37, enlarge: 18, reserved: ['48','6D','6E','6F','70','98','BD','CD'], ignored: ['0C'] // ignored: mark delimiter tsheg bstar }, 'tirhuta': {link: 'Tirhuta_(Unicode_block)', prefix: 114, blocks: [8,9,'A','B','C','D'], trim: 6, reserved: ['C8','C9','CA','CB','CC','CD','CE','CF'] }, 'warang citi': {link: 'Warang_Citi_(Unicode_block)', prefix: 118, blocks: ['A','B','C','D','E','F'], reserved: ['F3','F4','F5','F6','F7','F8','F9','FA','FB','FC','FD','FE'] }, }, // CYRILLIC 'cyrillic': { 'cyrillic': {link: 'Cyrillic_(Unicode_block)', prefix: '04', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F']}, 'cyrillic extended-a': {link: 'Cyrillic_Extended-A', prefix: '2D', blocks: ['E','F'], enlarge: 22, spacer: true}, 'cyrillic extended-b': {link: 'Cyrillic_Extended-B', prefix: 'A6', blocks: [4,5,6,7,8,9]}, 'cyrillic extended-c': {link: 'Cyrillic_Extended-C', prefix: '1C', blocks: [8], trim: 5}, 'cyrillic supplement': {link: 'Cyrillic_Supplement', prefix: '05', blocks: [0,1,2]}, 'glagolitic': {link: 'Glagolitic_(Unicode_block)', prefix: '2C', blocks: [0,1,2,3,4,5], enlarge: 14}, }, // EAST ASIAN 'east asian': { 'bopomofo': {link: 'Bopomofo_(Unicode_block)', prefix: 31, blocks: [0,1,2], reserved: ['00','01','02','03','04']}, 'bopomofo extended': {link: 'Bopomofo_Extended', prefix: 31, blocks: ['A','B'], enlarge: 14 }, 'cjk compatibility': {link: 'CJK_Compatibility', prefix: 33, //* partial: no changes since 2003, only 7 changes since 1993 blocks: [0,1,2,3], isPartial: true //*/ /* full blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'] //*/ }, 'cjk compatibility forms': {link: 'CJK_Compatibility_Forms', prefix: 'FE', blocks: [3,4]}, 'cjk compatibility ideographs': {link: 'CJK_Compatibility_Ideographs', prefix: 'F', reserved: ['A6E','A6F'], // partial: 5 changes out of 472 since 2005: all 5 changes are in A2 + A6 blocks: ['A2','A3','A4','A5','A6'], isPartial: true /* full blocks: [90,91,92,93,94,95,96,97,98,99,'9A','9B','9C','9D','9E','9F','A0','A1','A2','A3','A4','A5','A6','A7','A8','A9','AA','AB','AC','AD','AE','AF'], trim: 38, //*/ }, 'cjk compatibility ideographs supplement': {link: 'CJK_Compatibility_Ideographs_Supplement', prefix: '2F', //* partial: all v3.1 2001 blocks: [82,83,84,85], partial: true //*/ /* full blocks: [80,81,82,83,84,85,86,87,88,89,'8A','8B','8C','8D','8E','8F',90,91,92,93,94,95,96,97,98,99,'9A','9B','9C','9D','9E','9F','A0','A1'], trim: 2 //*/ }, 'cjk strokes': {link: 'CJK_Strokes_(Unicode_block)', prefix: 31, blocks: ['C','D','E'], reserved: ['E6','E7','E8','E9','EA','EB','EC','ED','EE'], ignored: ['EF'] // ignored character subtraction }, 'cjk symbols and punctuation': {link: 'CJK_Symbols_and_Punctuation', prefix: 30, blocks: [0,1,2,3], ignored: ['00']}, // ignored: ID SP 'cjk radicals supplement': {link: 'CJK_Radicals_Supplement', prefix: '2E', blocks: [8,9,'A','B','C','D','E','F'], trim: 12, reserved: ['9A']}, 'cjk unified ideographs': {link: 'CJK_Unified_Ideographs_(Unicode_block)', prefix: '9F', // partial: (from 20k+) includes all 90 changes since the v1.0.1 in 1992 blocks: ['A','B','C','D','E','F'], partial: true }, 'cjk unified ideographs extension-a': {link: 'CJK_Unified_Ideographs_Extension_A', prefix: '4D', // partial: (from 6.5k+) v3 1999 | v13 2020 = +10 | blocks include all 10 changes since the first version blocks: [9,'A','B','C'], partial: true }, 'cjk unified ideographs extension-b': {link: 'CJK_Unified_Ideographs_Extension_B', prefix: '2A6', // partial: (from 42k+) v3.1 2001 | v13+14 2020-21 = +9 | block A6D includes all 9 changes since the first version blocks: ['A','B','C','D'], partial: true }, 'enclosed cjk letters and months': {link: 'Enclosed_CJK_Letters_and_Months', prefix: '32', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], reserved: ['1F'] }, 'hangul compatibility jamo': {link: 'Hangul_Compatibility_Jamo', prefix: 31, blocks: [3,4,5,6,7,8], reserved: ['30'], ignored: ['64']}, // ignored: HF 'hangul jamo': {link: 'Hangul_Jamo_(Unicode_block)', prefix: 11, enlarge: 14, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], ignored: ['5F','60'] // ignored: HCF hangul choseong filler, HJF hangul jungseong filler }, 'hangul syllables': {link: 'Hangul_Syllables', prefix: 'AC', blocks: [0,1,2,3], partial: true}, // partial: (from 11k+) AFAICT no changes since intial v2 1996 'hiragana': {link: 'Hiragana_(Unicode_block)', prefix: 30, blocks: [4,5,6,7,8,9], reserved: ['40','97','98']}, 'ideographic description characters': {link: 'Ideographic_Description_Characters_(Unicode_block)', prefix: '2F', blocks: ['F']}, 'kanbun': {link: 'Kanbun_(Unicode_block)', prefix: 31, blocks: [9], enlarge: 16}, 'kangxi radicals': {link: 'Kangxi_Radicals_(Unicode_block)', prefix: '2F', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D'], trim: 10}, 'katakana': {link: 'Katakana_(Unicode_block)', prefix: 30, blocks: ['A','B','C','D','E','F']}, 'katakana phonetic extensions': {link: 'Katakana_Phonetic_Extensions', prefix: 31, blocks: ['F'], enlarge: 16}, 'lisu': {link: 'Lisu_(Unicode_block)', prefix: 'A4', blocks: ['D','E','F']}, 'vertical forms': {link: 'Vertical_Forms', prefix: 'FE', blocks: [1], trim: 6, enlarge: 16}, 'yi radicals': {link: 'Yi_Radicals', prefix: 'A4', blocks: [9,'A','B','C'], trim: 9}, 'yi syllables': {link: 'Yi_Syllables', prefix: 'A0', blocks: [0,1,2,3], partial: true, enlarge: 16}, // partial: (from 1k+) no changes since initial v3 1999 }, // GEORGIAN 'georgian': { 'georgian': {link: 'Georgian_(Unicode_block)', prefix: 10, blocks: ['A','B','C','D','E','F'], reserved: ['C6','C8','C9','CA','CB','CC','CE','CF']}, 'georgian extended': {link: 'Georgian_Extended', prefix: '1C', blocks: [9,'A','B'], reserved: ['BB','BC']}, 'georgian supplement': {link: 'Georgian_Supplement', prefix: '2D', blocks: [0,1,2], trim: 2, reserved: ['26','28','29','2A','2B','2C']}, }, // GREEK 'greek': { 'greek and coptic': {link: 'Greek_and_Coptic', prefix: '03', blocks: [7,8,9,'A','B','C','D','E','F'], reserved: ['78','79','80','81','82','83','8B','8D','A2'] }, 'greek extended': {link: 'Greek_Extended', prefix: '1F', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], trim: 1, reserved: ['16','17','1E','1F','46','47','4E','4F','58','5A','5C','5E','7E','7F','B5','C5','D4','D5','DC','F0','F1','F5'], }, }, // LATIN 'latin': { 'basic latin': {link: 'Basic_Latin_(Unicode_block)', prefix: '00', blocks: [0,1,2,3,4,5,6,7], ignored: [ '00','01','02','03','04','05','06','07','08','09','0A','0B','0C','0D','0E','0F', '10','11','12','13','14','15','16','17','18','19','1A','1B','1C','1D','1E','1F', // first 2 rows '20','7F', // ignored: SP, DEL ] }, 'latin-1 supplement': {link: 'Latin-1_Supplement', prefix: '00', blocks: [8,9,'A','B','C','D','E','F'], ignored: [ '80','81','82','83','84','85','86','87','88','89','8A','8B','8C','8D','8E','8F', '90','91','92','93','94','95','96','97','98','99','9A','9B','9C','9D','9E','9F', // first 2 rows 'A0','AD', // ignored: NBSP, SHY ] }, 'latin extended-a': {link: 'Latin_Extended-A', prefix: '01', blocks: [0,1,2,3,4,5,6,7]}, 'latin extended-b': {link: 'Latin_Extended-B', prefix: 0, blocks: [18,19,'1A','1B','1C','1D','1E','1F',20,21,22,23,24]}, 'latin extended-c': {link: 'Latin_Extended-C', prefix: '2C', blocks: [6,7]}, 'latin extended-d': {link: 'Latin_Extended-D', prefix: 'A7', blocks: [2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], reserved: ['DD','DE','DF','E0','E1','E2','E3','E4','E5','E6','E7','E8','E9','EA','EB','EC','ED','EE','EF','F0'] }, 'latin extended-e': {link: 'Latin_Extended-E', prefix: 'AB', blocks: [3,4,5,6], trim: 4}, 'latin extended-f': {link: 'Latin_Extended-F', prefix: 107, blocks: [8,9,'A','B'], trim: 5, reserved: ['86','B1']}, 'latin extended-g': {link: 'Latin_Extended-G', prefix: '1DF', blocks: [0,1,2], trim: 5 , reserved: ['1F','20','21','22','23','24']}, 'latin extended additional': {link: 'Latin_Extended_Additional', prefix: '1E', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F']}, }, // PHONETIC 'phonetic': { 'ipa extensions': {link: 'IPA_Extensions', prefix: '02', blocks: [5,6,7,8,9,'A'], enlarge: 14}, 'phonetic extensions': {link: 'Phonetic_Extensions', prefix: '1D', blocks: [0,1,2,3,4,5,6,7], enlarge: 16}, 'phonetic extensions supplement': {link: 'Phonetic_Extensions_Supplement', prefix: '1D', blocks: [8,9,'A','B'], enlarge: 16}, 'spacing modifier letters': {link: 'Spacing_Modifier_Letters', prefix: '02', blocks: ['B','C','D','E','F'], enlarge: 18}, }, // SEMITIC 'semitic': { 'arabic': {link: 'Arabic_(Unicode_block)', prefix: '06', enlarge: 16, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], ignored: ['1C'] // ignored ALM }, 'arabic extended-a': {link: 'Arabic_Extended-A', prefix: '08', blocks: ['A','B','C','D','E','F'], enlarge: 16, ignored: ['E2'] // ignored ARABIC DISPUTED END OF AYAH }, 'arabic extended-b': {link: 'Arabic_Extended-B', prefix: '08', blocks: [7,8,9], enlarge: 16, reserved: ['92','93','94','95','96'], ignored: ['90','91'] }, 'arabic extended-c': {link: 'Arabic_Extended-C', prefix: '10E', blocks: ['C','D','E','F'], enlarge: 16, reserved: ['C0','C1','C8','C9','CA','CB','CC','CD','CE','CF','D9','DA','DB','DC','DD','DE','DF', 'E0','E1','E2','E3','E4','E5','E6','E7','E8','E9','EA','EB','EC','ED','EE','EF', // all this row 'F0','F1','F2','F3','F4','F5','F6','F7','F8','F9', ] }, 'arabic presentation forms-a': {link: 'Arabic_Presentation_Forms-A', prefix: 'F', enlarge: 16, // partial: (from 650+) 45 changes since v6 2010: +20 in 2021, +25 in 2025 | blocks includes all 45 changes since 2010 // selective: blocks couldn't be contiguous to get the 45 changes blocks: ['B5','B6','BC','BD','D4','D9','DC','DF'], partial: true, selective: true }, 'arabic supplement': {link: 'Arabic_Supplement', prefix: '07', blocks: [5,6,7], enlarge: 16}, 'hebrew': {link: 'Hebrew_(Unicode_block)', prefix: '05', blocks: [9,'A','B','C','D','E','F'], trim: 11, enlarge: 18, reserved: ['90','C8','C9','CA','CB','CC','CD','CE','CF','EB','EC','ED','EE'] }, 'mandaic': {link: 'Mandaic_(Unicode_block)', prefix: '08', blocks: [4,5], trim: 1, reserved: ['5C','5D']}, 'samaritan': {link: 'Samaritan_(Unicode_block)', prefix: '08', blocks: [0,1,2,3], trim: 1, reserved: ['2E','2F']}, 'syriac': {link: 'Syriac_(Unicode_block)', prefix: '07', blocks: [0,1,2,3,4], reserved: ['0E','4B','4C'], enlarge: 16}, }, // SYMBOLS 'symbols': { 'alphabetic presentation forms': {link: 'Alphabetic_Presentation_Forms', prefix: 'FB', blocks: [0,1,2,3,4], enlarge: 14, reserved: ['07','08','09','0A','0B','0C','0D','0E','0F','10','11','12','18','19','1A','1B','1C','37','3D','3F','42','45'] }, 'arrows': {link: 'Arrows_(Unicode_block)', prefix: 21, blocks: [9,'A','B','C','D','E','F'], enlarge: 14}, 'block elements': {link: 'Block_Elements', prefix: 25, blocks: [8,9]}, 'box drawing': {link: 'Box_Drawing', prefix: 25, blocks: [0,1,2,3,4,5,6,7]}, 'braille patterns': {link: 'Braille_Patterns', prefix: 28, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F']}, 'currency symbols': {link: 'Currency_Symbols_(Unicode_block)', prefix: 20, blocks: ['A','B','C'], trim: 14}, 'dingbat': {link: 'Dingbats_(Unicode_block)', prefix: 27, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B']}, 'enclosed alphanumerics': {link: 'Enclosed_Alphanumerics', prefix: 24, blocks: [6,7,8,9,'A','B','C','D','E','F']}, 'general punctuation': {link: 'General_Punctuation', prefix: 20, blocks: [0,1,2,3,4,5,6], reserved: ['65'], enlarge: 16, ignored: [ '00','01','02','03','04','05','06','07','08','09','0A','0B','0C','0D','0E','0F', '06','61','62','63','64','66','67','68','69','6A','6B','6C','6D','6E','6F', // '65' is reserved '11', '28','29', '2A','2B','2C','2D','2E','2F','5F', // NB, L SEP, P SEP, LRE, RLE, PDF, LRO, RLO, NNB SP, MM SP ], }, 'geometric shapes': {link: 'Geometric_Shapes_(Unicode_block)', prefix: 25, blocks: ['A','B','C','D','E','F']}, 'letterlike symbols': {link: 'Letterlike_Symbols', prefix: 21, blocks: [0,1,2,3,4]}, 'mathematical alphanumeric symbols': {link: 'Mathematical_Alphanumeric_Symbols', prefix: '1D4', blocks: [1,2,3], partial: true // partial: (from ~1k) 5 changes since the initial v3.1 in 2001 | no changes since v5 in 2006 }, 'mathematical operators': {link: 'Mathematical_Operators_(Unicode_block)', prefix: 22, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'] }, 'miscellaneous mathematical symbols-a': {link: 'Miscellaneous_Mathematical_Symbols-A', prefix: 27, blocks: ['C','D','E']}, 'miscellaneous mathematical symbols-b': {link: 'Miscellaneous_Mathematical_Symbols-B', prefix: 29, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'] }, 'miscellaneous symbols': {link: 'Miscellaneous_Symbols', prefix: 26, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'] }, 'miscellaneous technical': {link: 'Miscellaneous_Technical', prefix: 23, blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], reserved: ['29','2A'], // 2 deprecated 5.2 }, 'number forms': {link: 'Number_Forms', prefix: 21, blocks: [5,6,7,8], trim: 4}, 'optical character recognition': {link: 'Optical_Character_Recognition_(Unicode_block)', prefix: 24, blocks: [4,5], trim: 21, enlarge: 18 }, 'superscripts and subscripts': {link: 'Superscripts_and_Subscripts_(Unicode_block)', prefix: 20, blocks: [7,8,9], trim: 3, reserved: ['72','73','8F'], enlarge: 18 }, 'supplemental arrows-a': {link: 'Supplemental_Arrows-A', prefix: 27, blocks: ['F'], enlarge: 14}, 'supplemental arrows-b': {link: 'Supplemental_Arrows-B', prefix: 29, blocks: [0,1,2,3,4,5,6,7], enlarge: 14}, 'supplemental arrows-c': {link: 'Supplemental_Arrows-C', prefix: '1F8', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'], trim: 39, reserved: ['0C','0D','0E','0F','48','49','4A','4B','4C','4D','4E','4F','5A','5B','5C','5D','5E','5F','88','89','8A','8B','8C','8D', '8E','8F','AE','AF','BC','BD','BE','BF','C2','C3','C4','C5','C6','C7','C8','C9','CA','CB','CC','CD','CE','CF'], }, 'supplemental mathematical operators': {link: 'Supplemental_Mathematical_Operators', prefix: '2A', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'] }, }, // output is sorted by keys // things that don't fit anywhere 'misc': { 'armenian': {link: 'Armenian_(Unicode_block)', prefix: '05', blocks: [3,4,5,6,7,8], reserved: ['30','57','58','8B','8C']}, 'combining diacritical marks': {link: 'Combining_Diacritical_Marks', spacer: true, enlarge: 20, prefix: '03', blocks: [0,1,2,3,4,5,6], ignored: ['4F'] // ignored: CGJ }, 'combining diacritical marks for symbols': {link: 'Combining_Diacritical_Marks_for_Symbols', spacer: true, enlarge: 14, prefix: 20, blocks: ['D','E','F'], trim: 15 }, 'combining diacritical marks supplement': {link: 'Combining_Diacritical_Marks_Supplement', spacer: true, enlarge: 20, prefix: '1D', blocks: ['C','D','E','F'] }, 'control pictures': {link: 'Control_Pictures', prefix: 24, blocks: [0,1,2,3], trim: 22, enlarge: 16}, 'halfwidth and fullwidth forms': {link: 'Halfwidth_and_Fullwidth_Forms_(Unicode_block)', prefix: 'FF', blocks: [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E'], trim: 1, reserved: ['00','BF','C0','C1','C8','C9','D0','D1','D8','D9','DD','DE','DF','E7'], ignored: ['A0'] // ignored: HW HF }, 'mongolian': {link: 'Mongolian_(Unicode_block)', prefix: 18, blocks: [0,1,2,3,4,5,6,7,8,9,'A'], trim: 5, enlarge: 18, reserved: ['1A','1B','1C','1D','1E','1F','79','7A','7B','7C','7D','7E','7F'], ignored: ['0B','0C','0D','0E','0F'], // ignored FVS1, FVS2, FVS3, MVS, FVS4 }, 'thanaa': {link: 'Thaana_(Unicode_block)', prefix: '07', blocks: [8,9,'A','B'], trim: 14, enlarge: 16}, }, } let oDisplay = {}, aSpacer = [] let ZWNJ = String.fromCodePoint('0x200C') +' ' let maxChars = 260, lastUsed, lastName, maxLanguages, maxScripts const dTarget = dom.dMeasure const sTarget = dom.sMeasure function strip(value) {return value.replace(/ /g,'')} // strip all spaces function shortcutKey(evt) { evt = evt || window.event if ('ArrowLeft' == evt.key) { cycle('back') } else if ('ArrowRight' == evt.key) { cycle('forward') } else if ('ArrowUp' == evt.key || 'ArrowDown' == evt.key) { // cycle between types let type = lastUsed == 'scripts' ? 'languages' : 'scripts' run(type) } } function cycle(dir, type) { // on android it's a PITA to use comboboxes // and we can add shortcut keys for desktop if (undefined == type) {type = lastUsed} let target = document.getElementById(type) if ('back' == dir) { let index = target.selectedIndex let lastIndex = ('script' == type ? maxScripts : maxLanguages) - 1 let newIndex = index == 0 ? lastIndex : index - 1 target.selectedIndex = newIndex } else { target.selectedIndex++ if (target.value == '') {target.selectedIndex++} // end of list, return to top } run(type) } function compare(lang, str) { sTarget.innerHTML = str let aLang = ['', lang] let aStyles = ['monospace','sans-serif','serif'] let data = {} aStyles.forEach(function(s){ data[s] = [] dTarget.style.setProperty('font-family', s) aLang.forEach(function(l){ dTarget.setAttribute('lang', l) // div height, span width let h = dTarget.getBoundingClientRect().height let w = sTarget.getBoundingClientRect().width data[s].push(w +' x '+ h) }) }) return data } function run_once() { // do once // populate languages let aLang = [], aFallback = [] for (const k of Object.keys(oSentence).sort()) { aLang.push('<option value = "'+ k +'"> '+ k +'</option>') aFallback.push(oSentence[k]) } maxLanguages = aLang.length dom.languages.innerHTML = aLang.join('') // display sentences to help somewhat with async font fasllback // since we measure languages let sentences = aFallback.join(' '+ ZWNJ +'<br>') dom.fallback.innerHTML = '<p class="mono s4">'+ sentences +'</p>' + '<p class="sans s8">'+ sentences +'</p>' + '<p class="serif s12">'+ sentences +'</p>' // get display string per script let oMaxChars = {} let aSuffix = [0,1,2,3,4,5,6,7,8,9,'A','B','C','D','E','F'] oDisplay = {} aSpacer = [] let aOptions = [] for (const group of Object.keys(oBlocks).sort()) { let anchor = strip(group) aOptions.push('<optgroup label = "'+ anchor.toUpperCase() +'">') for (const script of Object.keys(oBlocks[group]).sort()) { let name = script.toLowerCase() aOptions.push('<option value = "'+ name +'"> '+ name +'</option>') // set vars let data = oBlocks[group][script] let prefix = data.prefix, range = data.blocks, remove = data['trim'], reserved = data.reserved, ignored = data.ignored, spacer = data.spacer if (spacer) {aSpacer.push(name)} // fixup vars if (reserved == undefined) {reserved = []} if (ignored == undefined) {ignored = []} if (remove == undefined) {remove = 0} else (remove = (Math.abs(remove)) * -1) // ensure negative // loop let aChars = [] let tmpCodes = [], tmpChars = [], tmpReserved = [] for (let i = 0; i < range.length; i++) { for (let j = 0; j < aSuffix.length; j++) { let string = "0x"+ prefix + range[i] + aSuffix[j] let match = ""+ range[i] + aSuffix[j] if (!ignored.includes(match) && !reserved.includes(match)) { aChars.push(String.fromCodePoint(string)) } } } // trim results if (remove < 0) {aChars = aChars.slice(0, remove)} // limit size for display if (aChars.length > maxChars) { oMaxChars[name] = { 'count': aChars.length, 'discarded': aChars.slice(maxChars - aChars.length), 'kept': aChars.slice(0, maxChars) } aChars = aChars.slice(0, maxChars) } oDisplay[name] = aChars } } maxScripts = Object.keys(oDisplay).length if (Object.keys(oMaxChars).length) { console.log('scripts limited to first ' + maxChars +' chars\n', oMaxChars) } // populate scripts options dom.scripts.innerHTML = aOptions.join('') // display language try {dom.lang.innerHTML = navigator.language} catch(e){console.log(e)} // add listener document.addEventListener('keydown', shortcutKey) // run first option // use scripts to allow more time for font fallback for languages run('scripts') } function run(type) { lastUsed = type let name = document.getElementById(type).value if (lastName == name) {return} lastName = name const lang = name.split(' ')[0] const nolang = '<div class="faint">lang=""</div><div class="indent">' let display = '', str if ('scripts' == type) { dom.cmono.innerHTML = '' dom.csans.innerHTML = '' dom.cseri.innerHTML = '' // script if (oDisplay[name] !== undefined) { let spacer = aSpacer.includes(name) ? ' ' : '' display = '<span class="chars">'+ spacer + oDisplay[name].join(ZWNJ + spacer) + '</span>' } } else { // language str = oSentence[name] display = nolang + str +'</div>' + '<br><div class="faint" lang=\"'+ lang +'\">'+ 'lang="'+ lang +'"</div>' + '<div class="indent" lang="' + lang +'">' + str +'</div>' } dom.divInfo.innerHTML = name dom.divType.innerHTML = type dom.divMono.innerHTML = display dom.divSans.innerHTML = display dom.divSerif.innerHTML = display if ('languages' == type) { let data = compare(lang, str) for (const k of Object.keys(data)) { let isMatch = data[k][0] == data[k][1] let key = 'c' + k.slice(0,4) let target = dom[key] let datastr = isMatch ? data[k][0] : data[k].join(' | ') target.innerHTML = (isMatch ? s99 : s3) +'['+ datastr +']'+ sc } } } run_once() </script> </body> </html> ================================================ FILE: tests/functionprops.html ================================================ <!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>function properties</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 780px} </style> </head> <body> <span translate="no"> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#misc">return to TZP index</a></td></tr> </table> <table id="tb18"> <thead><tr><th> <div class="nav-title">function properties <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="c perf" id="totalCount"></span></div> </div> </th></tr></thead> <tr><td class="intro"> <span class="no_color"></span> <span class="btn18 btnfirst" onClick="run()">[ run ]</span> &nbsp; &nbsp; <select name="items" id="optList" onChange="run()"></select> &nbsp; &nbsp; <input type="checkbox" id="optSort"> <span class="no_color">sort </span> </td></tr> <tr><td><hr></td></tr> <tr><td></td></tr> <tr><td style="text-align: left; color: var(--test0);" class="mono spaces" id="details"></td></tr> </table> <br> <script> 'use strict'; let oData = {} let aEverything = [] let maxPad = 0 let stuff = { leftovers: [ // events 'AnimationEvent','AnimationPlaybackEvent','BeforeUnloadEvent','BlobEvent','ClipboardEvent', 'CloseEvent','CompositionEvent','ContentVisibilityAutoStateChangeEvent','CustomEvent', 'DragEvent','ErrorEvent','FocusEvent','FormDataEvent','HashChangeEvent','InputEvent', 'KeyboardEvent','MessageEvent','MutationEvent','PageTransitionEvent','PopStateEvent', 'PopupBlockedEvent','ProgressEvent','PromiseRejectionEvent','ScrollAreaEvent', 'SecurityPolicyViolationEvent','SubmitEvent','TaskPriorityChangeEvent','TimeEvent', 'ToggleEvent','TrackEvent','TransitionEvent','UIEvent','WheelEvent', 'Array','ArrayBuffer','DataView', 'BigInt','Boolean','Number','String', 'AbortController','AbortSignal', 'AbstractRange', 'Animation','AnimationEffect','AnimationTimeline', 'Attr', 'AuthenticatorAssertionResponse','AuthenticatorAttestationResponse','AuthenticatorResponse', 'Blob', 'BroadcastChannel', 'ByteLengthQueuingStrategy', // streams 'CDATASection', 'CaretPosition', 'CharacterData', 'Clipboard','ClipboardItem', 'Comment', 'CompressionStream', 'CountQueuingStrategy', // streams 'Credential','CredentialsContainer', 'Crypto','CryptoKey', 'CustomElementRegistry', 'DOMException', 'DOMImplementation', 'DOMMatrix', 'DOMMatrixReadOnly', 'DOMParser', 'DOMPoint','DOMPointReadOnly', 'DOMQuad','DOMRect','DOMRectList','DOMRectReadOnly', 'DOMRequest', 'DOMStringList', 'DOMStringMap', 'DOMTokenList', 'DataTransfer','DataTransferItem','DataTransferItemList', 'Date', 'DecompressionStream', 'Directory', 'Document', 'DocumentFragment', 'DocumentTimeline', 'DocumentType', 'Event', 'EventCounts', 'EventSource', 'EventTarget', 'File', 'FileList', 'FileReader', 'FinalizationRegistry', 'FormData', 'Headers', 'Highlight','HighlightRegistry', 'History', 'IdentityCredential', 'IdleDeadline', 'Image','ImageBitmap','ImageBitmapRenderingContext','ImageData', 'IntersectionObserver','IntersectionObserverEntry', 'KeyframeEffect', 'LargestContentfulPaint', 'Location', // the url 'Lock', 'LockManager', 'Map', 'MessageChannel','MessagePort', 'MutationObserver','MutationRecord', 'NamedNodeMap', 'NavigationPreloadManager', 'Node', 'NodeIterator', 'NodeList', 'Object', 'PaintRequest','PaintRequestList', 'PermissionStatus','Permissions', 'ProcessingInstruction', 'Promise', 'PublicKeyCredential', 'PushManager','PushSubscription','PushSubscriptionOptions', 'RadioNodeList', 'Range', 'ReadableByteStreamController','ReadableStream','ReadableStreamBYOBReader','ReadableStreamBYOBRequest','ReadableStreamDefaultController','ReadableStreamDefaultReader', 'RegExp', 'Request', 'ResizeObserver','ResizeObserverEntry','ResizeObserverSize', 'Response', 'Scheduler', 'Screen','ScreenOrientation', 'Selection', 'Set', 'ShadowRoot', 'SharedWorker', 'StaticRange', 'SubtleCrypto', 'Symbol', 'TaskController', 'TaskSignal', 'Text', 'TextDecoder','TextDecoderStream','TextEncoder','TextEncoderStream', 'TimeRanges', 'TransformStream', 'TransformStreamDefaultController', 'TreeWalker', 'URL', 'URLSearchParams', 'UserActivation', 'ValidityState', 'VisualViewport', 'WakeLock','WakeLockSentinel', 'WeakMap','WeakRef','WeakSet', 'WebSocket', 'WebTransport','WebTransportBidirectionalStream','WebTransportDatagramDuplexStream','WebTransportReceiveStream','WebTransportSendStream', 'Worklet', 'WritableStream','WritableStreamDefaultController','WritableStreamDefaultWriter', 'XMLDocument', 'XMLHttpRequest', 'XMLHttpRequestEventTarget', 'XMLHttpRequestUpload', 'XMLSerializer', 'XPathEvaluator', 'XPathExpression', 'XPathResult', 'XSLTProcessor', /* 'BigInt64Array','BigUint64Array','Float32Array','Float64Array','Int16Array','Int32Array','Int8Array','Uint16Array','Uint32Array','Uint8Array','Uint8ClampedArray', // ^ all = BYTES_PER_ELEMENT, constructor 'BarProp', // obsolete 'SVGMatrix','WebKitCSSMatrix' // aliases of DOMMatrix 'Window', // = constructor */ ]} //console.log(stuff.leftovers.length) let oList = { /* "TEMPLATE": { pad: 0, data: { 1: { title: 'main', desc: '', prefix: '', items: [ ], }, }, }, */ "_testing": { pad: 0, data: { 1: { title: 'devices', items: [ 'DeviceMotionEvent','DeviceOrientationEvent', 'Gamepad','GamepadAxisMoveEvent','GamepadButton','GamepadButtonEvent','GamepadEvent','GamepadHapticActuator','GamepadPose', 'MediaDeviceInfo','MediaDevices', 'MimeType','MimeTypeArray', 'MouseEvent','MouseScrollEvent', 'Plugin','PluginArray', 'PointerEvent', 'SpeechSynthesis','SpeechSynthesisErrorEvent','SpeechSynthesisEvent','SpeechSynthesisUtterance','SpeechSynthesisVoice', ], }, 2: { title: 'fonts', items: [ 'FontFace','FontFaceSet','FontFaceSetLoadEvent','TextMetrics', ], }, 3: { title: "region", items: [ 'Geolocation','GeolocationCoordinates','GeolocationPosition','GeolocationPositionError',] }, 4: { title: 'errors', items: [ 'AggregateError','Error','EvalError','GPUError','InternalError','RangeError', 'ReferenceError','SyntaxError','TypeError','URIError','WebTransportError', ], }, }, }, "All": { pad: 0, data: { "a": {items: ['AbortController','AbortSignal','AbstractRange','AggregateError','AnalyserNode','Animation','AnimationEffect','AnimationEvent','AnimationPlaybackEvent','AnimationTimeline','Array','ArrayBuffer','Attr','Audio','AudioBuffer','AudioBufferSourceNode','AudioContext','AudioDestinationNode','AudioListener','AudioNode','AudioParam','AudioParamMap','AudioProcessingEvent','AudioScheduledSourceNode','AudioWorklet','AudioWorkletNode','AuthenticatorAssertionResponse','AuthenticatorAttestationResponse','AuthenticatorResponse',]}, "b": {items: ['BarProp','BaseAudioContext','BeforeUnloadEvent','BigInt','BigInt64Array','BigUint64Array','BiquadFilterNode','Blob','BlobEvent','Boolean','BroadcastChannel','ByteLengthQueuingStrategy',]}, "c": {items: ['CDATASection','CSS2Properties','CSSAnimation','CSSConditionRule','CSSContainerRule','CSSCounterStyleRule','CSSFontFaceRule','CSSFontFeatureValuesRule','CSSFontPaletteValuesRule','CSSGroupingRule','CSSImportRule','CSSKeyframeRule','CSSKeyframesRule','CSSLayerBlockRule','CSSLayerStatementRule','CSSMediaRule','CSSMozDocumentRule','CSSNamespaceRule','CSSPageRule','CSSPropertyRule','CSSRule','CSSRuleList','CSSStyleDeclaration','CSSStyleRule','CSSStyleSheet','CSSSupportsRule','CSSTransition','Cache','CacheStorage','CanvasCaptureMediaStream','CanvasGradient','CanvasPattern','CanvasRenderingContext2D','CaretPosition','ChannelMergerNode','ChannelSplitterNode','CharacterData','Clipboard','ClipboardEvent','ClipboardItem','CloseEvent','Comment','CompositionEvent','CompressionStream','ConstantSourceNode','ContentVisibilityAutoStateChangeEvent','ConvolverNode','CountQueuingStrategy','Credential','CredentialsContainer','Crypto','CryptoKey','CustomElementRegistry','CustomEvent',]}, "d": {items: ['DOMException','DOMImplementation','DOMMatrix','DOMMatrixReadOnly','DOMParser','DOMPoint','DOMPointReadOnly','DOMQuad','DOMRect','DOMRectList','DOMRectReadOnly','DOMRequest','DOMStringList','DOMStringMap','DOMTokenList','DataTransfer','DataTransferItem','DataTransferItemList','DataView','Date','DecompressionStream','DelayNode','DeviceMotionEvent','DeviceOrientationEvent','Directory','Document','DocumentFragment','DocumentTimeline','DocumentType','DragEvent','DynamicsCompressorNode',]}, "e": {items: ['Element','ElementInternals','EncodedVideoChunk','Error','ErrorEvent','EvalError','Event','EventCounts','EventSource','EventTarget',]}, "f": {items: ['File','FileList','FileReader','FileSystem','FileSystemDirectoryEntry','FileSystemDirectoryHandle','FileSystemDirectoryReader','FileSystemEntry','FileSystemFileEntry','FileSystemFileHandle','FileSystemHandle','FileSystemWritableFileStream','FinalizationRegistry','Float32Array','Float64Array','FocusEvent','FontFace','FontFaceSet','FontFaceSetLoadEvent','FormData','FormDataEvent','Function',]}, "g": {items: ['GPU','GPUAdapter','GPUAdapterInfo','GPUBindGroup','GPUBindGroupLayout','GPUBuffer','GPUCanvasContext','GPUCommandBuffer','GPUCommandEncoder','GPUCompilationInfo','GPUCompilationMessage','GPUComputePassEncoder','GPUComputePipeline','GPUDevice','GPUDeviceLostInfo','GPUError','GPUInternalError','GPUOutOfMemoryError','GPUPipelineLayout','GPUQuerySet','GPUQueue','GPURenderBundle','GPURenderBundleEncoder','GPURenderPassEncoder','GPURenderPipeline','GPUSampler','GPUShaderModule','GPUSupportedFeatures','GPUSupportedLimits','GPUTexture','GPUTextureView','GPUUncapturedErrorEvent','GPUValidationError','GainNode','Gamepad','GamepadAxisMoveEvent','GamepadButton','GamepadButtonEvent','GamepadEvent','GamepadHapticActuator','GamepadPose','Geolocation','GeolocationCoordinates','GeolocationPosition','GeolocationPositionError',]}, "h": {items: ['HTMLAllCollection','HTMLAnchorElement','HTMLAreaElement','HTMLAudioElement','HTMLBRElement','HTMLBaseElement','HTMLBodyElement','HTMLButtonElement','HTMLCanvasElement','HTMLCollection','HTMLDListElement','HTMLDataElement','HTMLDataListElement','HTMLDetailsElement','HTMLDialogElement','HTMLDirectoryElement','HTMLDivElement','HTMLDocument','HTMLElement','HTMLEmbedElement','HTMLFieldSetElement','HTMLFontElement','HTMLFormControlsCollection','HTMLFormElement','HTMLFrameElement','HTMLFrameSetElement','HTMLHRElement','HTMLHeadElement','HTMLHeadingElement','HTMLHtmlElement','HTMLIFrameElement','HTMLImageElement','HTMLInputElement','HTMLLIElement','HTMLLabelElement','HTMLLegendElement','HTMLLinkElement','HTMLMapElement','HTMLMarqueeElement','HTMLMediaElement','HTMLMenuElement','HTMLMetaElement','HTMLMeterElement','HTMLModElement','HTMLOListElement','HTMLObjectElement','HTMLOptGroupElement','HTMLOptionElement','HTMLOptionsCollection','HTMLOutputElement','HTMLParagraphElement','HTMLParamElement','HTMLPictureElement','HTMLPreElement','HTMLProgressElement','HTMLQuoteElement','HTMLScriptElement','HTMLSelectElement','HTMLSlotElement','HTMLSourceElement','HTMLSpanElement','HTMLStyleElement','HTMLTableCaptionElement','HTMLTableCellElement','HTMLTableColElement','HTMLTableElement','HTMLTableRowElement','HTMLTableSectionElement','HTMLTemplateElement','HTMLTextAreaElement','HTMLTimeElement','HTMLTitleElement','HTMLTrackElement','HTMLUListElement','HTMLUnknownElement','HTMLVideoElement','HashChangeEvent','Headers','Highlight','HighlightRegistry','History',]}, "i": {items: ['IDBCursor','IDBCursorWithValue','IDBDatabase','IDBFactory','IDBIndex','IDBKeyRange','IDBObjectStore','IDBOpenDBRequest','IDBRequest','IDBTransaction','IDBVersionChangeEvent','IIRFilterNode','IdentityCredential','IdleDeadline','Image','ImageBitmap','ImageBitmapRenderingContext','ImageData','InputEvent','Int16Array','Int32Array','Int8Array','InternalError','IntersectionObserver','IntersectionObserverEntry',]}, "k": {items: ['KeyboardEvent','KeyframeEffect',]}, "l": {items: ['LargestContentfulPaint','Location','Lock','LockManager',]}, "m": {items: ['MIDIAccess','MIDIConnectionEvent','MIDIInput','MIDIInputMap','MIDIMessageEvent','MIDIOutput','MIDIOutputMap','MIDIPort','Map','MathMLElement','MediaCapabilities','MediaCapabilitiesInfo','MediaDeviceInfo','MediaDevices','MediaElementAudioSourceNode','MediaEncryptedEvent','MediaError','MediaKeyError','MediaKeyMessageEvent','MediaKeySession','MediaKeyStatusMap','MediaKeySystemAccess','MediaKeys','MediaList','MediaMetadata','MediaQueryList','MediaQueryListEvent','MediaRecorder','MediaRecorderErrorEvent','MediaSession','MediaSource','MediaStream','MediaStreamAudioDestinationNode','MediaStreamAudioSourceNode','MediaStreamEvent','MediaStreamTrack','MediaStreamTrackAudioSourceNode','MediaStreamTrackEvent','MessageChannel','MessageEvent','MessagePort','MimeType','MimeTypeArray','MouseEvent','MouseScrollEvent','MutationEvent','MutationObserver','MutationRecord',]}, "n": {items: ['NamedNodeMap','NavigationPreloadManager','Navigator','Node','NodeIterator','NodeList','Notification','Number',]}, "o": {items: ['Object','OfflineAudioCompletionEvent','OfflineAudioContext','OffscreenCanvas','OffscreenCanvasRenderingContext2D','Option','OscillatorNode',]}, "p": {items: ['PageTransitionEvent','PaintRequest','PaintRequestList','PannerNode','Path2D','Performance','PerformanceEntry','PerformanceEventTiming','PerformanceMark','PerformanceMeasure','PerformanceNavigation','PerformanceNavigationTiming','PerformanceObserver','PerformanceObserverEntryList','PerformancePaintTiming','PerformanceResourceTiming','PerformanceServerTiming','PerformanceTiming','PeriodicWave','PermissionStatus','Permissions','Plugin','PluginArray','PointerEvent','PopStateEvent','PopupBlockedEvent','ProcessingInstruction','ProgressEvent','Promise','PromiseRejectionEvent','PublicKeyCredential','PushManager','PushSubscription','PushSubscriptionOptions',]}, "r": {items: ['RTCCertificate','RTCDTMFSender','RTCDTMFToneChangeEvent','RTCDataChannel','RTCDataChannelEvent','RTCDtlsTransport','RTCEncodedAudioFrame','RTCEncodedVideoFrame','RTCIceCandidate','RTCPeerConnection','RTCPeerConnectionIceEvent','RTCRtpReceiver','RTCRtpScriptTransform','RTCRtpSender','RTCRtpTransceiver','RTCSctpTransport','RTCSessionDescription','RTCStatsReport','RTCTrackEvent','RadioNodeList','Range','RangeError','ReadableByteStreamController','ReadableStream','ReadableStreamBYOBReader','ReadableStreamBYOBRequest','ReadableStreamDefaultController','ReadableStreamDefaultReader','ReferenceError','RegExp','Request','ResizeObserver','ResizeObserverEntry','ResizeObserverSize','Response',]}, "s": {items: ['SVGAElement','SVGAngle','SVGAnimateElement','SVGAnimateMotionElement','SVGAnimateTransformElement','SVGAnimatedAngle','SVGAnimatedBoolean','SVGAnimatedEnumeration','SVGAnimatedInteger','SVGAnimatedLength','SVGAnimatedLengthList','SVGAnimatedNumber','SVGAnimatedNumberList','SVGAnimatedPreserveAspectRatio','SVGAnimatedRect','SVGAnimatedString','SVGAnimatedTransformList','SVGAnimationElement','SVGCircleElement','SVGClipPathElement','SVGComponentTransferFunctionElement','SVGDefsElement','SVGDescElement','SVGElement','SVGEllipseElement','SVGFEBlendElement','SVGFEColorMatrixElement','SVGFEComponentTransferElement','SVGFECompositeElement','SVGFEConvolveMatrixElement','SVGFEDiffuseLightingElement','SVGFEDisplacementMapElement','SVGFEDistantLightElement','SVGFEDropShadowElement','SVGFEFloodElement','SVGFEFuncAElement','SVGFEFuncBElement','SVGFEFuncGElement','SVGFEFuncRElement','SVGFEGaussianBlurElement','SVGFEImageElement','SVGFEMergeElement','SVGFEMergeNodeElement','SVGFEMorphologyElement','SVGFEOffsetElement','SVGFEPointLightElement','SVGFESpecularLightingElement','SVGFESpotLightElement','SVGFETileElement','SVGFETurbulenceElement','SVGFilterElement','SVGForeignObjectElement','SVGGElement','SVGGeometryElement','SVGGradientElement','SVGGraphicsElement','SVGImageElement','SVGLength','SVGLengthList','SVGLineElement','SVGLinearGradientElement','SVGMPathElement','SVGMarkerElement','SVGMaskElement','SVGMatrix','SVGMetadataElement','SVGNumber','SVGNumberList','SVGPathElement','SVGPatternElement','SVGPoint','SVGPointList','SVGPolygonElement','SVGPolylineElement','SVGPreserveAspectRatio','SVGRadialGradientElement','SVGRect','SVGRectElement','SVGSVGElement','SVGScriptElement','SVGSetElement','SVGStopElement','SVGStringList','SVGStyleElement','SVGSwitchElement','SVGSymbolElement','SVGTSpanElement','SVGTextContentElement','SVGTextElement','SVGTextPathElement','SVGTextPositioningElement','SVGTitleElement','SVGTransform','SVGTransformList','SVGUseElement','SVGViewElement','Scheduler','Screen','ScreenOrientation','ScriptProcessorNode','ScrollAreaEvent','SecurityPolicyViolationEvent','Selection','ServiceWorker','ServiceWorkerContainer','ServiceWorkerRegistration','Set','ShadowRoot','SharedWorker','SourceBuffer','SourceBufferList','SpeechSynthesis','SpeechSynthesisErrorEvent','SpeechSynthesisEvent','SpeechSynthesisUtterance','SpeechSynthesisVoice','StaticRange','StereoPannerNode','Storage','StorageEvent','StorageManager','String','StyleSheet','StyleSheetList','SubmitEvent','SubtleCrypto','Symbol','SyntaxError',]}, "t": {items: ['TaskController','TaskPriorityChangeEvent','TaskSignal','Text','TextDecoder','TextDecoderStream','TextEncoder','TextEncoderStream','TextMetrics','TextTrack','TextTrackCue','TextTrackCueList','TextTrackList','TimeEvent','TimeRanges','ToggleEvent','TrackEvent','TransformStream','TransformStreamDefaultController','TransitionEvent','TreeWalker','TypeError',]}, "u": {items: ['UIEvent','URIError','URL','URLSearchParams','Uint16Array','Uint32Array','Uint8Array','Uint8ClampedArray','UserActivation',]}, "v": {items: ['VTTCue','VTTRegion','ValidityState','VideoColorSpace','VideoDecoder','VideoFrame','VideoPlaybackQuality','VisualViewport',]}, "w": {items: ['WakeLock','WakeLockSentinel','WaveShaperNode','WeakMap','WeakRef','WeakSet','WebGL2RenderingContext','WebGLActiveInfo','WebGLBuffer','WebGLContextEvent','WebGLFramebuffer','WebGLProgram','WebGLQuery','WebGLRenderbuffer','WebGLRenderingContext','WebGLSampler','WebGLShader','WebGLShaderPrecisionFormat','WebGLSync','WebGLTexture','WebGLTransformFeedback','WebGLUniformLocation','WebGLVertexArrayObject','WebKitCSSMatrix','WebSocket','WebTransport','WebTransportBidirectionalStream','WebTransportDatagramDuplexStream','WebTransportError','WebTransportReceiveStream','WebTransportSendStream','WheelEvent','Window','Worker','Worklet','WritableStream','WritableStreamDefaultController','WritableStreamDefaultWriter',]}, "x": {items: ['XMLDocument','XMLHttpRequest','XMLHttpRequestEventTarget','XMLHttpRequestUpload','XMLSerializer','XPathEvaluator','XPathExpression','XPathResult','XSLTProcessor',]}, }, }, "Audio": { pad: 0, data: { 1: { prefix: 'Audio', ignore: [ 'Audio','AnalyserNode','BaseAudioContext','BiquadFilterNode','ChannelMergerNode','ChannelSplitterNode', 'ConvolverNode','ConstantSourceNode','DelayNode','DynamicsCompressorNode','GainNode','IIRFilterNode', 'MediaElementAudioSourceNode','OfflineAudioContext','OfflineAudioCompletionEvent','OscillatorNode', 'PannerNode','PeriodicWave','ScriptProcessorNode','StereoPannerNode','WaveShaperNode', ], items: [ 'Buffer','BufferSourceNode','Context','DestinationNode','Listener','Node', 'Param','ParamMap','ProcessingEvent','ScheduledSourceNode','Worklet','WorkletNode', ], }, }, }, "Canvas": { pad: 0, data: { 1: { prefix: 'Canvas', ignore: ['OffscreenCanvas','OffscreenCanvasRenderingContext2D','Path2D',], items: ['CaptureMediaStream','Gradient','Pattern','RenderingContext2D',], }, }, }, "CSS": { pad: 0, data: { 1: { prefix: 'CSS', suffix: 'Rule', ignore: [ 'CSSAnimation','CSSRule','CSSRuleList','CSSStyleDeclaration','CSSStyleSheet','CSSTransition','StyleSheet','StyleSheetList', ], items: [ 'Condition','Container','CounterStyle','FontFace','FontFeatureValues','FontPaletteValues','Grouping','Import','Keyframe', 'Keyframes','LayerBlock','LayerStatement','Media','MozDocument','Namespace','Page','Property','Style','Supports', ], }, }, }, "Element": { pad: 0, data: { 4: { // changes since FF104 (but stable since FF111 - that is not in current ESR115 cycle) // last checked 122 desc: "checkin'", items: ['ElementInternals',], }, 5: { // changes since FF115 (at least on windows) or exhibit extension fuckery // future = beta/nightly/experiment = will eventually land title: 'main', desc: "changes since FF115 | show extension tampering", prefix: 'HTML', suffix: 'Element', ignore: [ 'Element', // future: setHTMLUnsafe, getBoxQuads, convertQuadFromNode, convertRectFromNode, convertPointFromNode // 119: role, 41 x aria* 'HTMLElement', // future: showPopover, hidePopover, togglePopover, popover, onbeforetoggle, -ondragexit // 117: oncancel 'MathMLElement', // 117: onauxclick, -oncancel, -onauxclick, ], items: [ 'Button', // future: popoverTargetElement, popoverTargetAction 'Dialog', // new 98 // 139: requestClose (1960556) 'Details', // 130n: name (1856460: dom.details_group.enabled) 'Frame', // ext fuckery 'IFrame', // 121: loading 'Image', // 126n: 1882548 fetchPriority 'Input', // 116: dirName 'Link', // 126n: 1882548 fetchPriority 'Marquee', // added 65 // 126: 1689705 removed -onbounce, -onfinish, -onstart 'Media', // future: allowedToPlay // 116: setSinkId, sinkId 'Object', // ext fuckery 'Select', // 122b: showPicker 'Script', // 126n: 1882548 fetchPriority 'Template', // future: shadowRootMode, shadowRootDelegatesFocus 'TextArea', // 116: dirName 'Video', // 122b: disablePictureInPicture ], }, 6: { // changes since FF104 (but stable since FF111 - that is not in current ESR115 cycle) // last checked 122 title: 'recent', desc: "changes since FF104 (stable since FF111)", prefix: 'HTML', suffix: 'Element', items: [ 'Canvas', // last change 105 = cf7b9afc 'Form', // last change 111 = bff84eef 'Meta', // last change 106 = d32c1bc0 'Source', // last change 108 = e74ef3df ], }, 7: { // stable since FF92 title: 'stable92', desc: 'AFAICT FF92+', prefix: 'HTML', suffix: 'Element', items: [ 'Body', 'FrameSet', 'Menu', // changed 85 'Meter', 'Output', 'Param', // changed 85 'Progress', 'Slot', // new in 63, changed 66 92 'Style', ], }, 8: { // stable since FF70 title: 'stable70', desc: 'AFAICT FF70+', ignore: [ 'HTMLAllCollection', // changed 64 'HTMLDocument' // changed 57, 58, 60, 61, 68, 69, 70 ], }, 9: { // stable since FF52 title: 'stable52', desc: 'AFAICT FF52+ | since items added', prefix: 'HTML', suffix: 'Element', ignore: ['HTMLCollection','HTMLFormControlsCollection','HTMLOptionsCollection'], // 'Option', // = same as HTMLOptionElement items: [ 'Anchor','Area','Audio','BR','Base','DList','Data','DataList', 'Directory','Div','Embed','FieldSet','Font','HR','Head','Heading', 'Html','Label','LI','Legend','Map', 'Mod','OList','OptGroup','Option', 'Paragraph','Picture','Pre','Quote','Span', 'Table','TableCaption','TableCell','TableCol','TableRow', 'TableSection','Time','Title','Track','UList','Unknown', ], }, }, }, "GPU": { pad: 0, data: { 1: { prefix: 'GPU', ignore: ['GPU'], items: [ 'Adapter','AdapterInfo','BindGroup','BindGroupLayout','Buffer','CanvasContext','CommandBuffer', 'CommandEncoder','CompilationInfo','CompilationMessage','ComputePassEncoder','ComputePipeline', 'Device','DeviceLostInfo','InternalError','OutOfMemoryError','PipelineLayout','QuerySet', 'Queue','RenderBundle','RenderBundleEncoder','RenderPassEncoder','RenderPipeline','Sampler', 'ShaderModule','SupportedFeatures','SupportedLimits','Texture','TextureView','UncapturedErrorEvent', 'ValidationError', ], }, }, }, "Media": { // webcodecs pad: 0, data: { 1: { title: 'media', prefix: 'Media', ignore: ['SourceBuffer','SourceBufferList',], items: [ 'Capabilities','CapabilitiesInfo','EncryptedEvent', 'Error','KeyError','KeyMessageEvent','KeySession','KeyStatusMap','KeySystemAccess','Keys','List', 'Metadata','QueryList','QueryListEvent','Recorder','RecorderErrorEvent','Session','Source', 'Stream','StreamAudioDestinationNode','StreamAudioSourceNode','StreamEvent','StreamTrack', 'StreamTrackAudioSourceNode','StreamTrackEvent', ], }, 2: { title: 'midi', prefix: 'MIDI', items: [ 'Access','ConnectionEvent','Input','InputMap','MessageEvent','Output','OutputMap','Port', ], }, 5: { title: 'video', prefix: 'Video', ignore: [ 'EncodedVideoChunk','TextTrack','TextTrackCue','TextTrackCueList','TextTrackList','VTTCue','VTTRegion', ], items: [ 'ColorSpace','Decoder','Frame','PlaybackQuality', ], }, }, }, "Performance": { pad: 0, data: { 1: { prefix: 'Performance', ignore: ['Performance'], items: [ 'Entry','EventTiming','Mark','Measure','Navigation','NavigationTiming','Observer', 'ObserverEntryList','PaintTiming','ResourceTiming','ServerTiming','Timing', ], }, }, }, "Storage": { pad: 0, data: { 4: { title: 'filesystem', prefix: 'FileSystem', ignore: ['FileSystem',], items: [ 'DirectoryEntry','DirectoryHandle','DirectoryReader','Entry', 'FileEntry','FileHandle','Handle','WritableFileStream', ], }, 5: { title: 'idb', prefix: 'IDB', items: [ 'Cursor','CursorWithValue','Database','Factory','Index','KeyRange','ObjectStore', 'OpenDBRequest','Request','Transaction','VersionChangeEvent', ], }, 8: { title: 'storage', ignore: [ 'Cache','CacheStorage', 'Storage','StorageEvent','StorageManager', ], }, 9: { title: 'workers', ignore: [ 'Notification','ServiceWorker','ServiceWorkerContainer','ServiceWorkerRegistration','Worker', ], }, }, }, "SVG": { pad: 0, data: { 1: { prefix: 'SVG', items: [ 'AElement','Angle','AnimateElement','AnimateMotionElement','AnimateTransformElement','AnimatedAngle', 'AnimatedBoolean','AnimatedEnumeration','AnimatedInteger','AnimatedLength','AnimatedLengthList', 'AnimatedNumber','AnimatedNumberList','AnimatedPreserveAspectRatio','AnimatedRect','AnimatedString', 'AnimatedTransformList','AnimationElement','CircleElement','ClipPathElement','ComponentTransferFunctionElement', 'DefsElement','DescElement','Element','EllipseElement','FEBlendElement','FEColorMatrixElement', 'FEComponentTransferElement','FECompositeElement','FEConvolveMatrixElement','FEDiffuseLightingElement', 'FEDisplacementMapElement','FEDistantLightElement','FEDropShadowElement','FEFloodElement','FEFuncAElement', 'FEFuncBElement','FEFuncGElement','FEFuncRElement','FEGaussianBlurElement','FEImageElement','FEMergeElement', 'FEMergeNodeElement','FEMorphologyElement','FEOffsetElement','FEPointLightElement','FESpecularLightingElement', 'FESpotLightElement','FETileElement','FETurbulenceElement','FilterElement','ForeignObjectElement', 'GElement','GeometryElement','GradientElement','GraphicsElement','ImageElement','Length','LengthList', 'LineElement','LinearGradientElement','MPathElement','MarkerElement','MaskElement','MetadataElement', 'Number','NumberList','PathElement','PatternElement','Point','PointList','PolygonElement','PolylineElement', 'PreserveAspectRatio','RadialGradientElement','Rect','RectElement','SVGElement','ScriptElement','SetElement', 'StopElement','StringList','StyleElement','SwitchElement','SymbolElement','TSpanElement','TextContentElement', 'TextElement','TextPathElement','TextPositioningElement','TitleElement','Transform','TransformList', 'UseElement','ViewElement', ], }, }, }, "WebGL": { pad: 0, data: { 1: { prefix: 'WebGL', items: [ '2RenderingContext','ActiveInfo','Buffer','ContextEvent','Framebuffer','Program', 'Query','Renderbuffer','RenderingContext','Sampler','Shader','ShaderPrecisionFormat', 'Sync','Texture','TransformFeedback','UniformLocation','VertexArrayObject', ], }, }, }, "WebRTC": { pad: 0, data: { 1: { prefix: 'RTC', items: [ 'Certificate','DTMFSender','DTMFToneChangeEvent','DataChannel','DataChannelEvent','DtlsTransport', 'EncodedAudioFrame','EncodedVideoFrame','IceCandidate','PeerConnection','PeerConnectionIceEvent', 'RtpReceiver','RtpScriptTransform','RtpSender','RtpTransceiver','SctpTransport','SessionDescription', 'StatsReport','TrackEvent', ], }, }, }, } function build_list() { let optItems = [] for (const n of Object.keys(oList)) { let count = 0 optItems.push(n) let kdata = oList[n].data let max = 0 for (const k of Object.keys(kdata)) { let expected = 0 // include ignore items let tmpItems = [] // add prefix/suffix to items if (kdata[k].items !== undefined) { expected = expected + kdata[k].items.length let prefix = (kdata[k].prefix !== undefined) ? kdata[k].prefix : "" let suffix = (kdata[k].suffix !== undefined) ? kdata[k].suffix : "" // add prefix/suffix to items kdata[k].items.forEach(function(item) { tmpItems.push(prefix + item + suffix) }) } // _then_ add ignore items if (kdata[k].ignore !== undefined) { expected = expected + kdata[k].ignore.length kdata[k].ignore.forEach(function(item) { tmpItems.push(item) }) } // get padding tmpItems.forEach(function(i) { if (i.length > max) {max = i.length} }) // check for dupes let oldCount = tmpItems.length tmpItems = tmpItems.filter(function(item, position) {return tmpItems.indexOf(item) === position}) let newCount = tmpItems.length if (newCount !== oldCount) { console.error(n, k, "has duplicates") } // check expected numbers if (newCount !== expected) { console.error(n, k, "expected", expected, "got", newCount) } // replace items with new sorted list kdata[k].items = tmpItems.sort() count = count + newCount } oList[n].pad = max + 4 oList[n]["count"] = count if (n.length > maxPad) {maxPad = n.length} } // populate combobox optItems.sort() let optList = dom.optList optItems.forEach(function(opt) { var el = document.createElement("option") el.textContent = opt el.value = opt optList.appendChild(el) }) // set a default if (optItems.includes("Element")) { optList.value = "Element" } else { optList.value = optItems[0] } } function find(prop) { let aFound = [] for (const k of Object.keys(oData)) { if (oData[k].includes(prop)) { aFound.push(k) } } if (aFound.length) { return aFound } return "not found" } function run() { // https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API let t0 = performance.now() let optSort = dom.optSort.checked let optList = dom.optList.value let tmpList = oList[optList] let tmpObj = tmpList.data let padlen = tmpList.pad let tmpData ={}, tmpPost = {}, aDisplay = [], tmpDisplay = [] try { for (const k of Object.keys(tmpObj).sort()) { let groupdata = tmpObj[k] if (groupdata.items !== undefined) { let name, group if (optList.toLowerCase() == "all") { name = k group = k } else { name = (groupdata.title !== undefined ? groupdata.title.toLowerCase() : "") group = k + name } tmpData[group] = {} tmpDisplay = [] groupdata.items.forEach(function(item) { if (window[item] !== undefined) { try { let array = Object.getOwnPropertyNames(window[item].prototype) // post constructor let strPost = "", aPost = [], tmpArray = [], strData let isPost = (array[array.length -1] !== "constructor") if (isPost) { aPost = array.slice(array.indexOf("constructor")+1) strPost = " "+ sb + "("+ aPost.length +")"+ sc } // now sort array if (optSort) {array.sort()} let hash = mini(array) tmpData[group][item] = array // count + color each post item individually, record post data if (isPost) { array.forEach(function(n) { tmpArray.push(n) }) aPost.forEach(function(item) { tmpArray[tmpArray.indexOf(item)] = sb + item + sc }) tmpPost[item] = aPost strData = tmpArray.join(", ") } else { strData = array.join(", ") } tmpDisplay.push( s18 + item.padStart(padlen) + sc + ": "+ hash + s18 +" ("+ tmpData[group][item].length +")"+ sc + (isPost ? strPost : "") +"<br>"+ "<span class='toghidden" + k +" hidden faint'>[<br>" + "<span class='indent'>" + strData +"</span>" + "<br>]</span>" ) } catch(e) { console.log(item, e+"") } } else { console.log(item, "is undefined") } }) // calculate group hashes + notate let hash = mini(tmpData[group]), notation = "" if (isFF && !optSort) { if (optList === "Element") { if (k == "6" && isVer > 110) { notation = (hash == "b0cb1af2") ? green_tick : ' '+ zNEW } else if (k == "7" && isVer > 91) { notation = (hash == "642e531a") ? green_tick : ' '+ zNEW } else if (k == "8" && isVer > 69) { notation = hash == "eb335cf8" ? green_tick : ' '+ zNEW } else if (k == "9") { notation = hash == "1be9c8dc" ? green_tick : ' '+ zNEW } } } // display: add group header + details let strDesc = (groupdata.desc !== undefined && groupdata.desc.length) ? " <span class='faint'>... "+ groupdata.desc +"</span>" : "" aDisplay.push( "<br><hr>" + "<span id='labelhidden"+ k +"' class='btnfirst btn0' onClick=\"togglerows('hidden"+ k +"','expand')\">[ expand ]</span> " + s13 + hash + sc + notation +" "+ name.toUpperCase() + strDesc +"<br><br>" ) aDisplay.push(tmpDisplay.join("")) } } // post constructor items let oPost = {}, postCount = 0, strPost = "" for (const k of Object.keys(tmpPost).sort()) { oPost[k] = tmpPost[k] postCount += oPost[k].length } // finish perf here so I can optimize dom.perf = Math.round(performance.now() - t0) +" ms" // collect everything for testing purposes let newObj = {} aEverything = [] for (const k of Object.keys(tmpData)) { for (const j of Object.keys(tmpData[k])) { newObj[j] = tmpData[k][j] aEverything = aEverything.concat(tmpData[k][j]) } } aEverything = aEverything.filter(function(item, position) {return aEverything.indexOf(item) === position}) // deduped aEverything.sort() // order is artifical due to htmlList so lets remove that here //console.log("['"+ aEverything.join("','") +"']") // 447 // oData = sorted without subsections oData = {} for (const k of Object.keys(newObj).sort()) { oData[k] = newObj[k] } if (postCount > 0) { let posttoggle = "post" strPost = "<br>" + "<span id='labelhidden"+ posttoggle +"' class='btnfirst btn0' onClick=\"togglerows('hidden"+ posttoggle +"','expand')\">[ expand ]</span> " + s13 + mini(oPost) + sc + sb +" [" + postCount +"] "+ sc + "items after constructor" + "<span class='toghidden" + posttoggle +" hidden faint'>[<br>" + "<span class='indent'>" + json_highlight(oPost) +"</span>" + "<br>]</span>" +"<br>" } // display let strAll = s18 + (optList.toUpperCase()).padStart(maxPad) + sc +": "+ s13 + mini(oData) + sc + " <span class='btn18 btnc' onclick='console.log(oData)'>[console]</span> " + (optSort ? "sorted" : "unsorted") +" | unique values: " + s13 + mini(aEverything) + sc + " <span class='btn18 btnc' onclick='console.log(aEverything)'>[" + aEverything.length +"]</span>" dom.details.innerHTML = strAll +"<br>"+ strPost + aDisplay.join("") dom.totalCount = tmpList.count } catch(e) { dom.details = e+"" } } Promise.all([ get_globals() ]).then(function(){ Promise.all([ get_isVer() ]).then(function(){ build_list() run() }) }) </script> </span> </body> </html> ================================================ FILE: tests/math.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>math</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 780px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#misc">return to TZP index</a></td></tr> </table> <table id="tb18"> <col width="1%"><col width="9%"><col width="90%"> <thead><tr><th colspan="3">math: version 2.5</th></tr></thead> <tr><td colspan="3" class="intro"> <span class="no_color">Hashes all results, and compares those results to a control set, to build a list of tests that provides entropy. The (ongoing) results are <a class="blue" href="mathdata.html">here</a>.</span> </td></tr> <tr><td colspan="2"></td> <td> <span id="bfirefox" class="btn18 btnfirst" onClick="outputMath(`firefox`)">[ firefox ]</span> <span id="bchrome" class="btn18 btn" onClick="outputMath(`chrome`)">[ chrome ]</span> <span id="bsafari" class="btn18 btn" onClick="outputMath(`safari`)">[ safari ]</span> </td></tr> <tr><td colspan="3"><hr></td></tr> <tr><td colspan="3"><span id="perfE" class="mono"></span> <span class="btn0 btn" onClick="copyclip(`engine`)">[ copy ]</span> ENGINE</td></tr> <tr><td colspan="2" class="padr">engine</td><td class="mono spaces" id="engine"></td></tr> <tr><td colspan="3"></td></tr> <tr><td colspan="3"><hr></td></tr> <tr><td colspan="3"><span id="perf" class="c mono"></span> <span class="btn0 btn" onClick="copyclip(`report`)">[ copy ]</span> SHORT REPORT</td></tr> <tr><td colspan="2"></td><td class="c mono spaces" id="report"></td></tr> <tr><td colspan="3"></td></tr> <tr><td colspan="3"><hr></td></tr> <tr><td colspan="3">DIFFS ANALYSIS</td></tr> <tr><td colspan="2"></td><td class="s18">vs control</td></tr> <tr><td colspan="2"></td><td class="mono">denoted in <span class="sb">red</span></td></tr> <tr><td colspan="2" class="padr">function</td><td class="c mono spaces" id="diff1"></td></tr> <tr><td colspan="2" class="padr">polyfill</td><td class="c mono spaces" id="diff2"></td></tr> <tr><td colspan="2" class="padr">test no's</td><td class="c mono spaces" id="numbers"></td></tr> <tr><td colspan="3"></td></tr> <tr><td colspan="2"></td><td class="s18">polyfill vs function</td></tr> <tr><td colspan="2"></td><td class="mono">denoted with <span class="bad"> [match]</span></td></tr> <tr><td colspan="2" class="padr">diffs</td><td class="c mono spaces" id="diff0"></td></tr> <tr><td colspan="3"></td></tr> <tr><td colspan="3"><hr></td></tr> <tr><td colspan="3"><span class="btn0 btn" onClick="copyclip(`mathdata`)">[ copy ]</span> DATA</td></tr> <tr><td></td><td colspan="2" class="c mono spaces" id="mathdata"></td></tr> </table> <br> <script> 'use strict'; var fns = [], // what tests to run n = 0.123, bigN = 5.860847362277284e+38 // control sets var controlname = "" var setFF = [ '1.4474840516030247','0.7853981633974483','709.889355822726','Infinity','1.811526272460853','1.8115262724608532','0.881373587019543','0.8813735870195432','0.12331227519187199','691.4686750787736','691.4686750787736','1.8622957433108482','1.885022066017804','1.1071487177940904','1.2626272556789115','0.5493061443340548','0.5493061443340548','5e-311','1.0038848218538872','4.641588833612779','4.641588833612778','1.4645918875615231','1.4645918875615231','-0.37419577499634155','-0.7854805190645291','0.7914463018528902','-0.767224894221913','-0.7415825695514536','0.5403023058681397','0.7086865671674246','-0.7482651726250321','0.9924450321351935','-1','-0.10868049424995659','-0.8913089376870335','-0.7108118501064332','-0.5369116957490239','-0.40677759702517235','-0.7017203400855445','0.43628480636189976','-0.6982689820462376','-0.6982689820462376','-0.6534063185820198','0.45375574259827833','0.6459044007438142','1.5430806348152437','1.5430806348152437','11.591953275521519','11.591953275521519','9.199870313877774e+307','Infinity','1.046919966902314e+308','Infinity','1.718281828459045','1.718281828459045','22.140692632779267','22.140692632779267','1.1308844209474893','23.140692632779267','9.539392014169456','9.539392014169458','8.288489826731114e+38','8.288489826731116e+38','100.14767208675258','100.14767208675259','101.7610227859332','101.76102278593319','100.00960859865252','100.0096085986525','100.01040630344927','100.01040630344929','100.00999950005','100.00999950004999','100.00249996875078','100.0024999687508','100.00377216279418','100.00377216279416','-2.0955709236097197','1.1447298858494002','0.11600367575630613','0.11600367575630613','1.4210804127942926','1.4210804127942926','-0.9100948885606021','-0.9100948885606022','0.49714987269413385','0.49714987269413385','0.4342944819032518','0.4342944819032518','1.965773398945507','1.965773398945507','-0.1591745389548616','-0.1591745389548616','0.8822181462033635','0.8822181462033635','0.15917453895486158','0.15917453895486158','1.7926429945344482','1.792642994534448','-0.36221568869946325','-0.3622156886994632','0.48288235131479357','0.4828823513147936','-0.15051499783199057','-0.15051499783199057','0.15051499783199063','0.1505149978319906','0.15051499783199063','0.1505149978319906','-0.9273497301314576','-0.6188863822787813','-0.6112387023768895','0.6413781736901984','0.6708616046081811','0.8414709848078965','-0.7055234578073583','0.66339975236386','0.994076732536068','1.2246467991473532e-16','-0.7181630308570678','-0.765996413898051','0.9989410140273757','0.10135692924965614','-0.37463575478582023','-0.9892668187780497','1.1752011936438014','1.1752011936438014','11.548739357257748','11.548739357257748','7.544137102816975','7.544137102816975','0.75','0.75','1.9978980091062795','1.9978980091062797','9.199870313877774e+307','Infinity','0.44807597941469024','0.4480759794146903','0.7675231451261164','0.7675231451261164','1.935066822174357','1.9350668221743568','1.046919966902314e+308','Infinity','0.3507135583350036','1.7724538509055159','2.478247463217681','0.7879079967710036','-0.7723059681318761','-0.8359715365344825','-0.904635076595654','1.5574077246549023','-0.9955366596368418','-0.8865837628611647','0.5086861259107568','-1.2246467991473532e-16','0.686676154645243','1.618281713571588','-3.353712870537601','-1.922295546179998','-1.922295546179998','2.5824856130712437','0.12238344189440875','0.12238344189440872','0.99627207622075','0.9962720762207501','1.0220893335845176e+91','1.9275814160560185e-50','3.720075976020851e-44','8269017203802410','6.003867926738811e-37','1.2093335584550061e-16','1.665592934758592e+36','1125899906842611.5','8.881784197001154e-16' ] var setChrome = [ '1.4474840516030247','0.7853981633974483','709.889355822726','Infinity','1.811526272460853','1.8115262724608532','0.881373587019543','0.8813735870195432','0.12331227519187199','691.4686750787736','691.4686750787736','1.8622957433108482','1.885022066017804','1.1071487177940904','1.2626272556789115','0.5493061443340548','0.5493061443340548','5e-311','1.0038848218538872','4.641588833612779','4.641588833612778','1.4645918875615231','1.4645918875615234','-0.37419577499634155','-0.7854805190645291','0.7914463018528903','-0.767224894221913','-0.7415825695514535','0.5403023058681398','0.7086865671674247','-0.7482651726250322','0.9924450321351935','-1','-0.10868049424995659','-0.8913089376870335','-0.7108118501064331','-0.536911695749024','-0.4067775970251724','-0.7017203400855446','0.4362848063618998','-0.6982689820462377','-0.6982689820462377','-0.6534063185820198','0.4537557425982784','0.6459044007438142','1.5430806348152437','1.5430806348152437','11.591953275521519','11.591953275521519','9.199870313877772e+307','Infinity','1.0469199669023138e+308','Infinity','1.718281828459045','1.718281828459045','22.140692632779267','22.140692632779267','1.1308844209474893','23.140692632779267','9.539392014169456','9.539392014169458','8.288489826731116e+38','8.288489826731116e+38','100.14767208675259','100.14767208675259','101.76102278593319','101.76102278593319','100.0096085986525','100.0096085986525','100.01040630344929','100.01040630344929','100.00999950004999','100.00999950004999','100.0024999687508','100.0024999687508','100.00377216279416','100.00377216279416','-2.0955709236097197','1.1447298858494002','0.11600367575630613','0.11600367575630613','1.4210804127942926','1.4210804127942926','-0.9100948885606021','-0.9100948885606022','0.4971498726941338','0.49714987269413385','0.4342944819032518','0.4342944819032518','1.9657733989455068','1.965773398945507','-0.1591745389548616','-0.1591745389548616','0.8822181462033634','0.8822181462033635','0.15917453895486158','0.15917453895486158','1.792642994534448','1.792642994534448','-0.36221568869946325','-0.3622156886994632','0.4828823513147936','0.4828823513147936','-0.15051499783199057','-0.15051499783199057','0.1505149978319906','0.1505149978319906','0.1505149978319906','0.1505149978319906','-0.9273497301314576','-0.6188863822787813','-0.6112387023768895','0.6413781736901984','0.6708616046081811','0.8414709848078965','-0.7055234578073583','0.66339975236386','0.994076732536068','1.2246467991473532e-16','-0.7181630308570677','-0.7659964138980511','0.9989410140273756','0.10135692924965616','-0.3746357547858202','-0.9892668187780498','1.1752011936438014','1.1752011936438014','11.548739357257748','11.548739357257748','7.544137102816975','7.544137102816975','0.75','0.75','1.9978980091062795','1.9978980091062797','9.199870313877772e+307','Infinity','0.44807597941469024','0.4480759794146903','0.7675231451261164','0.7675231451261164','1.935066822174357','1.9350668221743568','1.0469199669023138e+308','Infinity','0.3507135583350036','1.7724538509055159','2.478247463217681','0.7879079967710036','-0.7723059681318761','-0.8359715365344825','-0.904635076595654','1.5574077246549023','-0.9955366596368418','-0.8865837628611647','0.5086861259107568','-1.2246467991473532e-16','0.6866761546452431','1.6182817135715877','-3.3537128705376014','-1.9222955461799982','-1.9222955461799982','2.5824856130712432','0.12238344189440875','0.12238344189440872','0.99627207622075','0.9962720762207501','1.022089333584519e+91','1.9275814160560204e-50','3.7200759760208555e-44','8269017203802394','6.003867926738829e-37','1.20933355845501e-16','1.6655929347585958e+36','1125899906842616.2','8.881784197001191e-16' ] var setSafari = [ '1.4474840516030245','0.7853981633974483','709.889355822726','Infinity','1.811526272460853','1.8115262724608532','0.8813735870195432','0.8813735870195432','0.12331227519187199','691.4686750787736','691.4686750787736','1.8622957433108482','1.885022066017804','1.1071487177940906','1.2626272556789115','0.5493061443340549','0.5493061443340549','5e-311','1.0038848218538872','4.641588833612779','4.641588833612778','1.4645918875615234','1.4645918875615231','-0.3741957749963415','-0.7854805190645292','0.7914463018528903','-0.7672248942219131','-0.7415825695514535','0.5403023058681398','0.7086865671674247','-0.7482651726250322','0.9924450321351935','-1','-0.10868049424995659','-0.8913089376870335','-0.7108118501064331','-0.536911695749024','-0.40677759702517235','-0.7017203400855446','0.4362848063618998','-0.6982689820462377','-0.6982689820462377','-0.6534063185820198','0.4537557425982784','0.6459044007438142','1.5430806348152437','1.5430806348152437','11.591953275521519','11.591953275521519','9.199870313877772e+307','Infinity','1.0469199669023138e+308','Infinity','1.7182818284590453','1.718281828459045','22.140692632779267','22.140692632779267','1.1308844209474893','23.140692632779267','9.539392014169456','9.539392014169458','8.288489826731116e+38','8.288489826731116e+38','100.14767208675259','100.14767208675259','101.76102278593319','101.76102278593319','100.0096085986525','100.0096085986525','100.01040630344929','100.01040630344929','100.00999950004999','100.00999950004999','100.0024999687508','100.0024999687508','100.00377216279416','100.00377216279416','-2.0955709236097197','1.1447298858494002','0.11600367575630613','0.11600367575630613','1.4210804127942926','1.4210804127942926','-0.9100948885606021','-0.9100948885606022','0.49714987269413385','0.49714987269413385','0.4342944819032518','0.4342944819032518','1.965773398945507','1.965773398945507','-0.1591745389548616','-0.1591745389548616','0.8822181462033635','0.8822181462033635','0.15917453895486158','0.15917453895486158','1.7926429945344482','1.792642994534448','-0.36221568869946325','-0.3622156886994632','0.48288235131479357','0.4828823513147936','-0.15051499783199057','-0.15051499783199057','0.15051499783199063','0.1505149978319906','0.15051499783199063','0.1505149978319906','-0.9273497301314577','-0.6188863822787812','-0.6112387023768895','0.6413781736901984','0.6708616046081811','0.8414709848078965','-0.7055234578073583','0.66339975236386','0.994076732536068','1.2246467991473532e-16','-0.7181630308570677','-0.765996413898051','0.9989410140273756','0.10135692924965616','-0.37463575478582023','-0.9892668187780498','1.1752011936438014','1.1752011936438014','11.548739357257746','11.548739357257748','7.544137102816975','7.544137102816975','0.75','0.75','1.9978980091062795','1.9978980091062797','9.199870313877772e+307','Infinity','0.44807597941469024','0.4480759794146903','0.7675231451261164','0.7675231451261164','1.9350668221743568','1.9350668221743568','1.0469199669023138e+308','Infinity','0.3507135583350036','1.7724538509055159','2.4782474632176816','0.7879079967710035','-0.772305968131876','-0.8359715365344824','-0.9046350765956541','1.557407724654902','-0.9955366596368417','-0.8865837628611646','0.5086861259107567','-1.2246467991473532e-16','0.686676154645243','1.6182817135715875','-3.353712870537602','-1.922295546179998','-1.922295546179998','2.5824856130712437','0.12238344189440876','0.12238344189440872','0.99627207622075','0.9962720762207501','1.022089333584519e+91','1.9275814160560206e-50','3.7200759760208555e-44','8269017203802394','6.003867926738829e-37','1.20933355845501e-16','1.6655929347585955e+36','1125899906842616.2','8.881784197001191e-16' ] function get_engine() { let et0 = performance.now() function cbrt(x) { try { let y = Math.pow(Math.abs(x), 1 / 3) return x < 0 ? -y : y } catch(e) { return "error" } } let res = [], engine = zNEW for(let i=0; i < 6; i++) { try { let fnResult = "unknown" if (i == 0) { fnResult = cbrt(Math.PI) // polyfill } else if (i == 1) { fnResult = Math.log10(7*Math.LOG10E) } else if (i == 2) { fnResult = Math.log10(2*Math.SQRT1_2) } else if (i == 3) { fnResult = Math.acos(0.123) } else if (i == 4) { fnResult = Math.acosh(Math.SQRT2) } else if (i == 5) { fnResult = Math.atan(2) } res.push(fnResult) } catch(e) { res.push("error") } } let hash = sha1(res.join()) if (hash == "ede9ca53efbb1902cc213a0beb692fe1e58f9d7a") {engine = "blink" } else if (hash == "05513f36d87dd78af60ab448736fd0898d36b7a9") {engine = "webkit" } else if (hash == "38172d9426d77af71baa402940bad1336d3091d0") {engine = "edgeHTML" } else if (hash == "36f067c652c8cfd9072580fca1f177f07da7ecf0") {engine = "trident" // also presto if we don't use `let` } else if (hash == "225f4a612fdca4065043a4becff76a87ab324a74") {engine = "gecko [a]" // abraham } else if (hash == "cb89002a8d6fabf859f679fd318dffda1b4ae0ea") {engine = "gecko [b]" // me } if (engine.substring(0,5) == "gecko") { // palemoon/basilisk: fails 53, passes 54 if ("function" !== typeof CSSMozDocumentRule && URL.prototype.hasOwnProperty("toJSON")) { engine += "<br><br>but wait.. I did some extra tests<br>" + s12.trim() +"real engine:" + sc + s18 +"goanna"+ sc } } dom.perfE.innerHTML = Math.round(performance.now() - et0) +" ms" dom.engine.innerHTML = s12.trim() + hash + sc +"<br> &middot; "+ res.join("<br> &middot; ") +"<br>"+ s12.trim() +"engine: "+ sc + engine } function build_controlset(engine) { let c = [] if (engine == "firefox") {c = setFF } else if (engine == "chrome") {c = setChrome } else if (engine == "safari") {c = setSafari} fns = [ // [item, function, value, description, control, poly1 control, poly2 control] [0,'acos', [n], n, c[0]], [1,'acos', [Math.SQRT1_2], "Math.SQRT1_2", c[1]], [0,'acosh', [1e308], "1e300", c[2], c[3]], [1,'acosh', [Math.PI], "Math.PI", c[4], c[5]], [2,'acosh', [Math.SQRT2], "Math.SQRT2", c[6], c[7]], [0,'asin', [n], n, c[8]], [0,'asinh', [1e300], "1e300", c[9], c[10]], [1,'asinh', [Math.PI], "Math.PI", c[11], c[12]], [0,'atan', [2], "2", c[13]], [1,'atan', [Math.PI], "Math.PI", c[14]], [0,'atanh', [0.5], "0.5", c[15], c[16]], [0,'atan2', [1e-310, 2], "1e-310, 2", c[17]], [1,'atan2', [Math.PI, 2], "Math.PI, 2", c[18]], [0,'cbrt', [100], "100", c[19], c[20]], [1,'cbrt', [Math.PI], "Math.PI", c[21], c[22]], // original TZP cos [0,'cos', [1e251], "1e251", c[23]], [1,'cos', [1e140], "1e140", c[24]], [2,'cos', [1e12], "1e12", c[25]], [3,'cos', [1e130], "1e130", c[26]], [4,'cos', [1e272], "1e272", c[27]], [5,'cos', [1e0], "1e0", c[28]], [6,'cos', [1e284], "1e284", c[29]], [7,'cos', [1e75], "1e75", c[30]], [8,'cos', [n], n, c[31]], [9,'cos', [Math.PI], "Math.PI", c[32]], [10,'cos', [bigN], "bigN", c[33]], // creep says unique in Tor [11,'cos', [-1e308], '-1e308', c[34]], [12,'cos', [13*Math.E], '13*Math.E', c[35]], [13,'cos', [57*Math.E], '57*Math.E', c[36]], [14,'cos', [21*Math.LN2], '21*Math.LN2', c[37]], [15,'cos', [51*Math.LN2], '51*Math.LN2', c[38]], [16,'cos', [21*Math.LOG2E], '21*Math.LOG2E', c[39]], [17,'cos', [25*Math.SQRT2], '25*Math.SQRT2', c[40]], [18,'cos', [50*Math.SQRT1_2], '50*Math.SQRT1_2', c[41]], [19,'cos', [21*Math.SQRT1_2], '21*Math.SQRT1_2', c[42]], [20,'cos', [17*Math.LOG10E], '17*Math.LOG10E', c[43]], [21,'cos', [2*Math.LOG10E], '2*Math.LOG10E', c[44]], [0,'cosh', [1], "1", c[45], c[46]], [1,'cosh', [Math.PI], "Math.PI", c[47], c[48]], [2,'cosh', [492*Math.LOG2E], '492*Math.LOG2E', c[49], c[50]], [3,'cosh', [502*Math.SQRT2], '502*Math.SQRT2', c[51], c[52]], [0,'expm1', [1], "1", c[53], c[54]], [1,'expm1', [Math.PI], "Math.PI", c[55], c[56]], [0,'exp', [n], n, c[57]], [1,'exp', [Math.PI], "Math.PI", c[58]], [0,'hypot', [1,2,3,4,5,6], "1,2,3,4,5,6", c[59], c[60]], [1,'hypot', [bigN, bigN], "bigN, bigN", c[61], c[62]], [2,'hypot', [2*Math.E,-100], '2*Math.E,-100', c[63], c[64]], [3,'hypot', [6*Math.PI,-100], '6*Math.PI,-100', c[65], c[66]], [4,'hypot', [2*Math.LN2,-100], '2*Math.LN2,-100', c[67], c[68]], [5,'hypot', [Math.LOG2E,-100], 'Math.LOG2E,-100', c[69], c[70]], [6,'hypot', [Math.SQRT2,-100], 'Math.SQRT2,-100', c[71], c[72]], [7,'hypot', [Math.SQRT1_2,-100], 'Math.SQRT1_2,-100',c[73], c[74]], [8,'hypot', [2*Math.LOG10E,-100], '2*Math.LOG10E,-100', c[75], c[76]], [0,'log', [n], n, c[77]], [1,'log', [Math.PI], "Math.PI", c[78]], [0,'log1p', [n], n, c[79], c[80]], [1,'log1p', [Math.PI], "Math.PI", c[81], c[82]], [0,'log10', [n], n, c[83], c[84]], [1,'log10', [Math.PI], "Math.PI", c[85], c[86]], [2,'log10', [Math.E], "Math.E", c[87], c[88]], [3,'log10', [34*Math.E], '34*Math.E', c[89], c[90]], [4,'log10', [Math.LN2], "Math.LN2", c[91], c[92]], [5,'log10', [11*Math.LN2], '11*Math.LN2', c[93], c[94]], [6,'log10', [Math.LOG2E], "Math.LOG2E", c[95], c[96]], [7,'log10', [43*Math.LOG2E], '43*Math.LOG2E', c[97], c[98]], [8,'log10', [Math.LOG10E], "Math.LOG10E", c[99], c[100]], [9,'log10', [7*Math.LOG10E], '7*Math.LOG10E', c[101], c[102]], [10,'log10', [Math.SQRT1_2], "Math.SQRT1_2", c[103], c[104]], [11,'log10', [2*Math.SQRT1_2], '2*Math.SQRT1_2', c[105], c[106]], [12,'log10', [Math.SQRT2], "Math.SQRT2", c[107], c[108]], // original TZP value but with sin [0,'sin', [1e251], "1e251", c[109]], [1,'sin', [1e140], "1e140", c[110]], [2,'sin', [1e12], "1e12", c[111]], [3,'sin', [1e130], "1e130", c[112]], [4,'sin', [1e272], "1e272", c[113]], [5,'sin', [1e0], "1e0", c[114]], [6,'sin', [1e284], "1e284", c[115]], [7,'sin', [1e75], "1e75", c[116]], [8,'sin', [bigN], "bigN", c[117]], // creep says unique in Tor [9,'sin', [Math.PI], "Math.PI", c[118]], // creep says unique in Tor [10,'sin', [39*Math.E], "39*Math.E", c[119]], [11,'sin', [35*Math.LN2], "35*Math.LN2", c[120]], [12,'sin', [110*Math.LOG2E], "110*Math.LOG2E", c[121]], [13,'sin', [7*Math.LOG10E], "7*Math.LOG10E", c[122]], [14,'sin', [35*Math.SQRT1_2], "35*Math.SQRT1_2", c[123]], [15,'sin', [21*Math.SQRT2], "21*Math.SQRT2", c[124]], [0,'sinh', [1], "1", c[125], c[126]], [1,'sinh', [Math.PI], "Math.PI", c[127], c[128]], [2,'sinh', [Math.E], "Math.E", c[129], c[130]], [3,'sinh', [Math.LN2], "Math.LN2", c[131], c[132]], [4,'sinh', [Math.LOG2E], "Math.LOG2E", c[133], c[134]], [5,'sinh', [492*Math.LOG2E], '492*Math.LOG2E', c[135], c[136]], [6,'sinh', [Math.LOG10E], "Math.LOG10E", c[137], c[138]], [7,'sinh', [Math.SQRT1_2], "Math.SQRT1_2", c[139], c[140]], [8,'sinh', [Math.SQRT2], "Math.SQRT2", c[141], c[142]], [9,'sinh', [502*Math.SQRT2], '502*Math.SQRT2', c[143], c[144]], [0,'sqrt', [n], n, c[145]], [1,'sqrt', [Math.PI], "Math.PI", c[146]], // original TZP with tan [0,'tan', [1e251], "1e251", c[147]], [1,'tan', [1e140], "1e140", c[148]], [2,'tan', [1e12], "1e12", c[149]], [3,'tan', [1e130], "1e130", c[150]], [4,'tan', [1e272], "1e272", c[151]], [5,'tan', [1e0], "1e0", c[152]], [6,'tan', [1e284], "1e284", c[153]], [7,'tan', [1e75], "1e75", c[154]], [8,'tan', [-1e308], "-1e308", c[155]], [9,'tan', [Math.PI], "Math.PI", c[156]], [10,'tan', [6*Math.E], '6*Math.E', c[157]], [11,'tan', [6*Math.LN2], '6*Math.LN2', c[158]], [12,'tan', [10*Math.LOG2E], '10*Math.LOG2E', c[159]], [13,'tan', [17*Math.SQRT2], '17*Math.SQRT2', c[160]], [14,'tan', [34*Math.SQRT1_2], '34*Math.SQRT1_2', c[161]], [15,'tan', [10*Math.LOG10E], '10*Math.LOG10E', c[162]], [0,'tanh', [n], n, c[163], c[164]], [1,'tanh', [Math.PI], "Math.PI", c[165], c[166]], [0,'pow', [n,-100], n+",-100", c[167]], [1,'pow', [Math.PI,-100], "Math.PI,-100", c[168]], [2,'pow', [Math.E,-100], "Math.E,-100", c[169]], [3,'pow', [Math.LN2,-100], "Math.LN2,-100", c[170]], [4,'pow', [Math.LN10,-100], "Math.LN10,-100", c[171]], [5,'pow', [Math.LOG2E,-100], "Math.LOG2E,-100", c[172]], [6,'pow', [Math.LOG10E,-100], "Math.LOG10E,-100", c[173]], [7,'pow', [Math.SQRT1_2,-100], "Math.SQRT1_2,-100", c[174]], [8,'pow', [Math.SQRT2,-100], "Math.SQRT2,-100", c[175]], ] } // POLYFILLS const acosh = x => Math.log(x + Math.sqrt(x * x - 1)) const asinh = x => { const absX = Math.abs(x) if (absX < Math.pow(2, -28)) { return x } const w = ( absX > Math.pow(2, 28) ? Math.log(absX) + Math.LN2 : absX > 2 ? Math.log(2 * absX + 1 / Math.sqrt(x * x + 1)) : Math.log1p(absX + (x * x) / (1 + Math.sqrt(1 + (x * x)))) ) return x > 0 ? w : -w } const atanh = x => Math.log((1 + x) / (1 - x)) / 2 function cbrt(x) { let y = Math.pow(Math.abs(x), 1 / 3) return x < 0 ? -y : y } const cosh = x => (Math.exp(x) + Math.exp(-x)) / 2 const expm1 = x => Math.exp(x) - 1 function hypot(array) { let i, s = 0, max = 0, isInfinity = false, len = array.length for (i = 0; i < len; ++i) { const arg = Math.abs(+array[i]) if (arg === Infinity) { isInfinity = true } if (arg > max) { s *= (max / arg) * (max / arg) max = arg } s += arg === 0 && max === 0 ? 0 : (arg / max) * (arg / max) } return isInfinity ? Infinity : (max === Infinity ? Infinity : max * Math.sqrt(s)) } const log1p = x => { const nearX = (x + 1) - 1 return ( x < -1 || x !== x ? NaN : x === 0 || x === Infinity ? x : nearX === 0 ? x : x * (Math.log(x + 1) / nearX) ) } const log2 = x => Math.log(x) * Math.LOG2E const log10 = x => Math.log(x) * Math.LOG10E const sinh = x => (Math.exp(x) - Math.exp(-x)) / 2 const tanh = x => { const a = Math.exp(+x) const b = Math.exp(-x) return a == Infinity ? 1 : b == Infinity ? -1 : (a - b) / (a + b) } function get_results() { let t0 = performance.now() // vars let display = [], prevFn = "start", demarc = false, strDemarc = "----------------------------------------------------", sColor = s18, vPad = 19, // value tested fPad = vPad + 7, // function + value rPad = 23, // result nPad = 5, // user number: 5 means k indents by 2, and k+"p1"/"p2" is covered in diffs bigNstr = "* bigN = " + bigN // BUILD TEST RESULTS let math1 = [], // functions math2 = [], // polyfills numbers = [], // numbers which make a difference numberchk = [], // make sure no duplicates polymatch = [], fnmatch = [], // do not match control diffs = [] // fn + poly don't match let generate = [] for(let i=0; i < fns.length; i++) { let fn = fns[i] let k = fn[0] // user defined number let kpad = (k.toString()).padStart(nPad) let fnResult = (Math[fn[1]](...fn[2])).toString() let fnValue = fn[3].toString() let fnControl = fn[4].toString() let fnName = fn[1].toString() numberchk.push(fnName+"-"+k.toString()) // fn result let isFnDiff = false if (fnResult !== fnControl) { isFnDiff = true fnmatch.push(k+":"+fnName+"("+fn[3]+"):"+fnControl +":"+fnResult) numbers.push(fnName +":"+ k.toString()) } math1.push(fnName+"("+fnValue+"):"+fnResult) generate.push("'"+fnResult+"'") // poly tests let poly1Result = "" let poly1Control = "" if (fn[5] !== undefined && fn[5] !== "") { poly1Control = fn[5].toString() if (fnName == "acosh") {poly1Result = acosh(...fn[2]) } else if (fnName == "asinh") {poly1Result = asinh(...fn[2]) } else if (fnName == "atanh") {poly1Result = atanh(...fn[2]) } else if (fnName == "cbrt") {poly1Result = cbrt(...fn[2]) } else if (fnName == "cosh" ) {poly1Result = cosh(...fn[2]) } else if (fnName == "expm1") {poly1Result = expm1(...fn[2]) } else if (fnName == "hypot") {poly1Result = hypot([...fn[2]]) // pass array } else if (fnName == "log1p") {poly1Result = log1p(...fn[2]) } else if (fnName == "log2" ) {poly1Result = log2(...fn[2]) } else if (fnName == "log10") {poly1Result = log10(...fn[2]) } else if (fnName == "sinh" ) {poly1Result = sinh(...fn[2]) } else if (fnName == "tanh" ) {poly1Result = tanh(...fn[2])} poly1Result = poly1Result.toString() //console.debug(k, fn[3], fnName, poly1Control, poly1Result) } // poly1 results let isPolyDiff = false, isDiff = false if (poly1Result !== "") { if (poly1Result !== poly1Control) { isPolyDiff = true polymatch.push(k+"p1:"+fnName+"("+fn[3]+"):"+poly1Control +":"+poly1Result) numbers.push(fnName +":"+ k.toString() +"p1") } math2.push(fnName+"("+fnValue+")polyfill1:"+poly1Result) generate.push("'"+poly1Result+"'") // does not match fn if (poly1Result !== fnResult) { isDiff = true diffs.push(k+"p1:"+fnName+"("+fn[3]+"):"+fnResult +":"+poly1Result) } } // color & pad if (isFnDiff) { fnResult = sb.trim() + fnResult.padStart(rPad) + sc } else { fnResult = fnResult.padStart(rPad) } if (isPolyDiff) { poly1Result = sb.trim() + poly1Result.padStart(rPad) + sc } else { poly1Result = poly1Result.padStart(rPad) } if (isDiff) { poly1Result += sb +" [match]"+ sc } if (poly1Result !== "") { fnResult += ", " + poly1Result } // demarcate each function if (prevFn !== fnName) {demarc = true} else {demarc = false} if (demarc == true) { display.push(sColor.trim() + strDemarc +"<br>"+ fnName.toUpperCase() + sc) } display.push(kpad +": "+ fnValue.padEnd(vPad) +": "+ fnResult) // remember last function prevFn = fn[1] } // generate control lists //console.log("CONTROL LIST:\n " + generate.join(",")) // NUMBER+NAME DUPE CHECK numberchk = numberchk.filter(function(item, position) {return numberchk.indexOf(item) === position}) if (fns.length !== numberchk.length) { console.debug("fns array has duplicate user defined test ids") } // NUMBERS let numstr = "none", mathPrev = "", mathNext = "", tmp_nums = [] if (numbers.length > 0) { let numdisplay = [] for(let i=0; i < numbers.length; i++) { let part0 = numbers[i].split(":")[0] let part1 = numbers[i].split(":")[1] part1 = part1.replace(/\p1/g, s12.trim() +"p1"+ sc) // build number string tmp_nums.push(part1) if (i < numbers.length - 1) { mathNext = numbers[(i+1)].split(":")[0] } else { mathNext = "end" } // next math function is diff: write data if (mathNext !== part0) { numdisplay.push(s12.trim() + part0.padStart(5) + sc +": "+ tmp_nums.join(", ")) tmp_nums = [] // reset } mathPrev = part0 } numstr = numdisplay.join("<br>") } dom.numbers.innerHTML = numstr // DIFFS FUNCTION function output_diffs(array, type) { let output = [] if (array.length > 0) { for(let i=0; i < array.length; i++) { let part0 = array[i].split(":")[0] part0 = (part0.toString()).padStart(nPad) let part1 = array[i].split(":")[1] part1 = (part1.toString()).padEnd(fPad) let part2 = array[i].split(":")[2] part2 = (part2.toString()).padStart(rPad) let part3 = array[i].split(":")[3] output.push(s12.trim() + part1 + sc +" s/be "+ part2 +" got "+ sb + part3 + sc) } document.getElementById("diff" + type).innerHTML = output.join("<br>") } else { document.getElementById("diff" + type).innerHTML = "none" } } // HASHES let hash1 = sha1(math1.join()) let hash2 = sha1(math2.join()) let math3 = math1.concat(math2) let hash0 = sha1(math3.join()) //console.debug(math3.join("\n")) let maincode = get_code(hash0) if (numstr == "none") { numstr = s12.trim() +" nos: "+ sc + numstr } // SHORT REPORT let report = s12.trim() +"control: "+ controlname +"<br>"+ strDemarc + sc +"<br>"+ s12.trim() +" hash: "+ sc + hash0 + maincode +"<br>"+ s12.trim() +" func: "+ sc + hash1 +"<br>"+ s12.trim() +" poly: "+ sc + hash2 +"<br>"+ numstr dom.report.innerHTML = report // LONG REPORT output_diffs(diffs, 0) output_diffs(fnmatch, 1) output_diffs(polymatch, 2) // DATA dom.mathdata.innerHTML = report +"<br><br>"+ bigNstr.padStart(52) +"<br>"+ display.join("<br>") // PERF dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } function get_code(hash) { let r = "" // FF // windows if (hash == "97c37c8c3b1de92333bf1993a7350cf785b2715d") {r="F1"} // Win7 64bit: FF68-82 64bit [CONTROL] else if (hash == "d9e8b78b68a5d1086af27ceec9315f0797ccec7c") {r="F2"} // Win7 64bit: FF68-82 32bit else if (hash == "06f9ca0df07d4836116143158b670c9d3a417cd4") {r="F3"} // Win7 64bit: TB68-78 64bit else if (hash == "dd277bcf4a12289404b0aab1ae43be060be5a12a") {r="F4"} // Win7 64bit: TB68-78 32bit // pending windows machine no 2 tests (which I know provide more entropy) // rampaa //else if (hash == "b0130c424be28aac4ed155c679c9baf951349b75") {r="something"} // Win10 64bit: FF80 64bit // hash: b0130c424be28aac4ed155c679c9baf951349b75 [NEW] // func: e8dd64a68e81d54fbaab58bd3b48cbe2179f6587 // poly: 0cde3749d8f342c858af1eef6b48f2e2be985440 // cos: 5, 7, 14, 16, 17, 18 // sin: 14 // tan: 12 // android else if (hash == "571b9a318869bd09dab3fd83fd88883a66188011") {r="F20"} // android 9 aarch64 | browserstacks: android 10, 11 else if (hash == "56c05c3f03fb55c3c046ad1b68d05d2392fe0c54") {r="F21"} // browserstacks: android 6,7,7.1,8,9 else if (hash == "da130b6da16b5ddffc42ebf56259c4746d01a3b6") {r="F22"} // browserstacks: android 9 else if (hash == "d246b2f0103139eb4407a7dfe4dda2af0bcca4e6") {r="F23"} // browserstacks: android 10 else if (hash == "a624571a642dbcb468f6d2d8203d0659d351bdc1") {r="F24"} // browserstacks: android 5 else if (hash == "02e1298182c58da58956df1342b525bd5aecfd7e") {r="F25"} // browserstacks: android 4.4 else if (hash == "66b3de14094fec47652f8ad203e6d8e6c9ee418f") {r="F26"} // android 8, browserstacks 8.1, chromeOS-32bit android 9 // linux else if (hash == "e6c7b364678e8ecec909088314318acb8430ee47") {r="F30"} else if (hash == "05ea8b9bed51553711e3bbaa552449d8ea40c4be") {r="F31"} // mac else if (hash == "84ad7bbb52342b6c224c0fe45977184bc943bb92") {r="F50"} // obsolete linux else if (hash == "2047b6e1afc4c6bbe55d061e33fa8026c7afd648") {r="F80"} // mint32: FF60-67 else if (hash == "ac4f01ae0d298a6ef48bb20aa594a77fb14b0968") {r="F81"} // mint32: TB60 // obsolete windows else if (hash == "3ada7d492e4fa7a7a6acea446fa24ec3ff370556") {r="F96"} // Win7 64bit: FF60-67 64bit [obsolete versions] else if (hash == "a8b5e74f336489c1df701a95650ab091ff6de436") {r="F97"} // Win7 64bit: FF60-67 32bit [obsolete versions] else if (hash == "8d09688f127d62709bbb5d47f7fc8c959ae151a8") {r="F98"} // Win7 64bit: TB60 64bit [obsolete versions] else if (hash == "4796b1d067dd047768dcd2e9ecd740c0ef353d31") {r="F99"} // Win7 64bit: TB60 32bit [obsolete versions] // CHROME else if (hash == "42550bb913168ddbb8be40f970adc51344017cad") { r="C1"} // SAFARI else if (hash == "4423be6d9bec5550ad38ea9dde7ef2daf0609e0f") { r="S1"} // iOS else if (hash == "2e9162520d6f90c28e24d0f2e538f79f503f8a66") { r="iOS1"} // EDGEHTML else if (hash == "a53608a3ac4caf531014dcd4cfaac425db405398") { r="E99"} // NEW if (r == "") {r = " "+ zNEW} else {r = s12+"["+ r +"]"+sc} return r } function outputMath(control) { // clear let items = document.getElementsByClassName("c mono") for (let i=0; i < items.length; i++) {items[i].textContent = ""} // control setBtn(control) if (control == "firefox") {controlname = s8 +"Firefox"+ sc +" 78 64bit on Windows 64bit" } else if (control == "chrome") {controlname = s8 +"Chrome"+ sc +" 85 64bit on Windows 64bit" } else if (control == "safari") {controlname = s8 +"Safari"+ sc +" 13.0.5 on macOS Catalina [VM 64bit on Windows 10 64bit]"} // build control & run build_controlset(control) // delay so user can see things being reset setTimeout(function(){ get_results() }, 170) } function setBtn(control) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn18") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ control) el.classList.add("btn8") el.classList.remove("btn18") } s8 = s8.trim() get_engine() outputMath("firefox") </script> </body> </html> ================================================ FILE: tests/mathdata.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>math data</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 780px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#misc">return to TZP index</a></td></tr> </table> <table id="tb18"> <col width="7%"><col width="93%"> <thead><tr><th colspan="2">math data</th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A record of which tests for each unique hash [or short code] differ from the Firefox control in <a href="math.html" class="blue">this</a> test</span> </td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2">[FF68+] UNIQUE</td></tr> <tr><td><span class="s12" id="countFF">FUNC</span></td><td id="firefox"></td></tr> <tr><td colspan="2"></td></tr> <!-- spacer--> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2">[ALL] UNIQUE</td></tr> <tr><td><span class="s12" id="countAll">FUNC</span></td><td id="numbers"></td></tr> <tr><td colspan="2"></td></tr> <!-- spacer--> <tr><td colspan="2"><hr></td></tr> <tr><td colspan="2">RESULTS</td></tr> <tr><td><span class="s12">CODE</span></td><td colspan="2">RESULTS</td></tr> <tr><td colspan="2"></td></tr> <!-- spacer--> <!-- insert rows here for results --> </table> <br> <script> 'use strict'; var dataset = [ // keep it clean and ", ". I trim for number crunching but not the result strings // windows ['group', 'firefox 68+'], ['header', 'windows'], ['F1', "97c37c8c3b1de92333bf1993a7350cf785b2715d", "nos: none, i'm the control", "tests: Win7/Win10-vm 64bit: FF68-82 64bit", ], ['F2', "d9e8b78b68a5d1086af27ceec9315f0797ccec7c", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21", "sin: 0, 1, 2, 3, 4, 6, 7, 8, 11, 12, 14", "tan: 0, 1, 2, 3, 4, 6, 7, 8, 10, 15", "tests: Win7/Win10-vm 64bit**: FF68-82 32bit | Win7-vm 32bit: FF68-82 32bit", ], ['F3', "06f9ca0df07d4836116143158b670c9d3a417cd4", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21", "sin: 0, 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 14", "tests: Win7/Win10-vm 64bit: TB68-78 64bit", ], ['F4', "dd277bcf4a12289404b0aab1ae43be060be5a12a", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21", "sin: 0, 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 14", "tan: 0, 1, 2, 3, 4, 6, 7, 8, 10, 15", "tests: Win7/Win10-vm 64bit: TB68-78 32bit | Win7-vm 32bit: TB68-78 32bit", ], // android ['header', 'android'], ['F20', "571b9a318869bd09dab3fd83fd88883a66188011", "cos: 5, 6, 13, 14, 15, 16, 17, 18, 20", "sin: 13, 14", "tan: 10, 11, 12, 13, 14, 15", "tests: android 9 aarch64 FF80 | android 10, 11 (browserstacks)", ], ['F21', "56c05c3f03fb55c3c046ad1b68d05d2392fe0c54", "cbrt: 1p1", "cos: 5, 6, 13, 14, 15 ,16, 17, 18, 20", "cosh: 0, 0p1", "expm1: 0p1", "sin: 13, 14", "sinh: 0p1", "tan: 10, 11, 12, 13, 14, 15", "tests: android 6, 7, 7.1, 8, 9 (browserstacks)", ], ['F22', "da130b6da16b5ddffc42ebf56259c4746d01a3b6", "cos: 5, 6, 13, 15 ,16, 17, 18, 20", "cosh: 0, 0p1", "expm1: 0p1", "sin: 13, 14", "sinh: 0p1", "tan: 10, 11, 13, 14, 15", "tests: android 9 (browserstacks)", ], ['F23', "d246b2f0103139eb4407a7dfe4dda2af0bcca4e6", "cos: 5, 6, 13, 14, 15 ,16, 17, 18, 20", "cosh: 0, 0p1", "expm1: 0p1", "sin: 13, 14", "sinh: 0p1", "tan: 10, 11, 12, 13, 14, 15", "tests: android 10 (browserstacks)", ], ['F24', "a624571a642dbcb468f6d2d8203d0659d351bdc1", "cbrt: 1p1", "cos: 5, 6, 7, 13, 14, 15 ,16, 17, 18, 20", "cosh: 0, 0p1", "expm1: 0p1", "sin: 13, 14", "sinh: 0p1", "tan: 10, 11, 12, 13, 14, 15", "tests: android 5 (browserstacks)", ], ['F25', "02e1298182c58da58956df1342b525bd5aecfd7e", "cos: 5, 6, 7, 13, 14, 15 ,16, 17, 18, 20", "cosh: 0, 0p1", "expm1: 0p1", "sin: 13, 14", "sinh: 0p1", "tan: 10, 11, 12, 13, 14, 15", "tests: android 4.4 (browserstacks)", ], ['F26', "66b3de14094fec47652f8ad203e6d8e6c9ee418f", "cbrt: 1p1", "cos: 5, 6, 13, 14, 15, 16, 17, 18, 20", "sin: 13, 14", "tan: 10, 11, 12, 13, 14, 15", "tests: android 8 | android 9 chromeOS-32bit | android 8.1 (browserstacks)", ], // linux ['header', 'linux'], ['F30', "e6c7b364678e8ecec909088314318acb8430ee47", "cos: 3, 4, 5, 6, 7, 13, 15, 16, 17, 18, 19, 20, 21", "sin: 11, 12, 14", "tan: 1, 10, 15", "tests: Linux 64bit: Debian (Buster) Fedora (v32), Manjaro (20.1), MX Linux: all FF80+", "tests: Tails 4.11: TB78", ], ['F31', "05ea8b9bed51553711e3bbaa552449d8ea40c4be", "cos: 4, 5, 6, 7, 13, 15, 16, 17, 18, 19, 20, 21", "sin: 11, 12, 14", "tan: 1, 10, 15", "tests: Mint 64bit FF78 | Mint-32bit FF68-82, TB68-78", ], // mac ['header', 'mac'], ['F50', "84ad7bbb52342b6c224c0fe45977184bc943bb92", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 12, 13, 15, 16, 17, 18, 20", "sin: 0, 1, 10, 12, 13, 15", "tan: 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12", "tests: macOS Catalina: FF80, TB68", ], // linux FF67 or lower: obsolete ['group', 'firefox 67-'], ['header', 'linux 67-'], ['F80', "2047b6e1afc4c6bbe55d061e33fa8026c7afd648", "cos: 4, 5, 6, 7, 13, 15, 16, 17, 18, 19, 20, 21", "cosh: 0, 0p1", "expm1: 0p1", "sin: 11, 12, 14", "sinh: 0p1", "tan: 1, 10, 15", "tests: Mint 32bit : FF60-67", ], ['F81', "ac4f01ae0d298a6ef48bb20aa594a77fb14b0968", "asinh: 0", "atan: 1", "atanh: 0, 0p1", "cos: 4, 5, 6, 7, 13, 15, 16, 17, 18, 19, 20, 21", "expm1: 0", "sin: 11, 12, 14", "sinh: 1, 8", "tan: 1, 10, 15", "tanh: 0", "pow: 0, 1, 2, 3, 4, 5, 6, 7, 8", "tests: Mint 32bit : TB60", ], // windows FF67 or lower: obsolete ['header', 'windows 67-'], ['F96', "3ada7d492e4fa7a7a6acea446fa24ec3ff370556", "cosh: 0, 0p1", "expm1: 0p1", "sinh: 0p1", "tests: Win7/Win10-vm 64bit: FF60-67 64bit", ], ['F97', "a8b5e74f336489c1df701a95650ab091ff6de436", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21", "cosh: 0, 0p1", "expm1: 0p1", "sin: 0, 1, 2, 3, 4, 6, 7, 8, 11, 12, 14", "sinh: 0p1", "tan: 0, 1, 2, 3, 4, 6, 7, 8, 10, 15", "tests: Win7/Win10-vm 64bit: FF60-67 32bit | Win7-vm 32bit: FF60-67 32bit", ], ['F98', "8d09688f127d62709bbb5d47f7fc8c959ae151a8", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21", "cosh: 0, 0p1", "expm1: 0p1", "sin: 0, 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 14", "sinh: 0p1", "tests: Win7/Win10-vm 64bit: TB60 64bit", ], ['F99', "4796b1d067dd047768dcd2e9ecd740c0ef353d31", "asinh: 0", "atan: 1", "atanh: 0, 0p1", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21", "expm1: 0", "sin: 0, 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 14", "sinh: 1, 8", "tan: 0, 1, 2, 3, 4, 6, 7, 8, 10, 15", "tanh: 0", "pow: 0, 1, 2, 3, 4, 5, 6, 7, 8", "tests: Win7/Win10-vm 64bit: TB60 32bit | Win7-vm 32bit: TB60 32bit", ], // non FF ['group', 'non-firefox'], ['header', 'chrome / chromium'], ['C1', "42550bb913168ddbb8be40f970adc51344017cad", "cbrt: 1p1", "cos: 2, 4, 5, 6, 7, 12, 13, 14, 15, 16, 17, 18, 20", "cosh: 2, 3", "hypot: 1, 2, 3, 4, 5, 6, 7, 8", "log10: 1, 3, 5, 7, 9, 11, 12", "sin: 10, 11, 12, 13, 14, 15", "sinh: 5, 9", "tan: 10, 11, 12, 13, 14, 15", "pow: 0, 1, 2, 3, 4, 5, 6, 7, 8", "tests: chrome: everywhere (windows, android, mac...) & everything (edge, opera, brave...)", ], ['header', 'safari'], ['S1', "4423be6d9bec5550ad38ea9dde7ef2daf0609e0f", "acos: 0", "acosh: 2", "atan: 0", "atanh: 0, 0p1", "cbrt: 1", "cos: 0, 1, 2, 3, 4, 5, 6, 7, 12, 13, 15, 16, 17, 18, 20", "cosh: 2, 3", "expm1: 0", "hypot: 1, 2, 3, 4, 5, 6, 7, 8", "sin: 0, 1, 10, 12, 13, 15", "sinh: 1, 5, 8, 9", "tan: 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12", "tanh: 0", "pow: 0, 1, 2, 3, 4, 5, 6, 7, 8", "tests: macOS Catalina Safari", ], ['header', 'iOS'], ['iOS1', "2e9162520d6f90c28e24d0f2e538f79f503f8a66", "acos: 0", "acosh: 2", "atan: 0", "atanh: 0, 0p1", "cbrt: 1", "cos: 1, 2, 4, 5, 6, 12, 13, 14, 15, 16, 17, 18, 20, 21", "cosh: 2, 3", "expm1: 0", "hypot: 1, 2, 3, 4, 5, 6, 7, 8", "sin: 11, 13, 14, 15", "sinh: 1, 5, 8, 9", "tan: 2, 3, 4, 5, 6, 7, 8, 11, 12", "tanh: 0", "pow: 0, 1, 2, 3, 4, 5, 6, 7, 8", "tests: webkit: Safari, Firefox, Chrome", ], ['header', 'EdgeHTML'], ['E99', "a53608a3ac4caf531014dcd4cfaac425db405398", "acosh: 1, 1p1", "atanh: 0, 0p1", "atan2: 1", "cbrt: 1", "cos: 0, 5, 7, 10, 11, 13, 16, 17, 18, 21", "cosh: 1, 1p1, 2, 3", "hypot: 1p1, 5p1, 6p1", "log: 1", "log10: 8, 9", "sin: 14", "sinh: 0, 0p1, 1, 4, 4p1, 5, 8p1, 9", "sqrt: 1", "tanh: 0", "pow: 0, 1, 2, 3, 4, 5, 6, 7, 8", "tests: Win10-vm 64bit EdgeHTML", ], ] function outputData() { var numbers = [], firefox = [], // firefox only numbers obsolete = [], // FF67- results firefoxall = [], // out of interest: FFonly vs FFall table = dom.tb18 // use consistent colors across similar tests var sColor1 = s3, // subheader sColor2 = s12, // hash, nos sColor3 = s14, // tests sColor4 = sg // TB dataset.forEach(function(data) { let code = data[0], str = data[1] if (code == "header") { str = sColor1 + str.toLowerCase() + sc let row = table.insertRow(-1) let a = row.insertCell(0) let b = row.insertCell(1) b.setAttribute("class", "mono spaces") b.innerHTML = str } else if (code == "group") { str = "<u>" + str.toUpperCase() +"</u>" let row = table.insertRow(-1) let a = row.insertCell(0) a.setAttribute("class", "mono spaces intro") a.colSpan = "2" a.innerHTML = str } else { let lines = [sColor2 +" hash: "+ sc + data[1]] for (let i=2; i < data.length; i++) { let fnc = data[i].split(":")[0] let fncPad = s12.trim() + fnc.padStart(5) +":"+ sc str = data[i].split(":")[1] if (fnc == "tests") { // configs tested str = data[i] // color up TB: boring list of replaces & lots of duplication :) str = str.replace(/TB68-78 64bit/g, sColor4 +"TB68-78 64bit"+ sc) str = str.replace(/TB68-78 32bit/g, sColor4 +"TB68-78 32bit"+ sc) str = str.replace(/TB60 64bit/g, sColor4 +"TB60 64bit"+ sc) str = str.replace(/TB60 32bit/g, sColor4 +"TB60 32bit"+ sc) str = str.replace(/TB68-78/g, sColor4 +"TB68-78"+ sc) str = str.replace(/TB78/g, sColor4 +"TB78"+ sc) str = str.replace(/TB68/g, sColor4 +"TB68"+ sc) str = str.replace(/TB60/g, sColor4 +"TB60"+ sc) lines.push(sColor3 + str + sc) } else { // results let strpretty = str.replace(/p1/g, sColor2 +"p1"+ sc) lines.push(fncPad + strpretty) // numbers: ignore F1 notation line ("nos") if (fnc !== "nos") { let tmp = str.split(",") tmp.forEach(function(num) { num = num.trim() // all numbers.push(fnc +" "+ num) // FF if (code.substring(0,1) == "F") { let second = code.substring(1,2) if (second == "8" || second == "9") { // obsolete FF obsolete.push(fnc +" "+ num) } else { // FF68+ firefox.push(fnc +" "+ num) } firefoxall.push(fnc +" "+ num) } }) } } } // add table row let row = table.insertRow(-1) let a = row.insertCell(0) let b = row.insertCell(1) a.innerHTML = code b.setAttribute("class", "mono spaces") b.innerHTML = lines.join("<br>") } }) // test number: sort them, de-dupe them, output them numbers.sort() firefox.sort() obsolete.sort() firefoxall.sort() numbers = numbers.filter(function(item, position) {return numbers.indexOf(item) === position}) firefox = firefox.filter(function(item, position) {return firefox.indexOf(item) === position}) obsolete = obsolete.filter(function(item, position) {return obsolete.indexOf(item) === position}) firefoxall = firefoxall.filter(function(item, position) {return firefoxall.indexOf(item) === position}) //console.debug(numbers.length, firefox.length, obsolete.length, firefoxall.length) // 110 55 61 72 // ToDo: maybe abraham wants all numbers excluding FF67- let strpretty = numbers.join(", ") strpretty = strpretty.replace(/p1/g, sColor2 +"p1"+ sc) dom.numbers.innerHTML = strpretty dom.countAll = numbers.length strpretty = firefox.join(", ") strpretty = strpretty.replace(/p1/g, sColor2 +"p1"+ sc) dom.firefox.innerHTML = strpretty dom.countFF = firefox.length // ToDo: number crunch a number set to bare minimum } outputData() </script> </body> </html> ================================================ FILE: tests/mathspoof.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>math spoof detection</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 480px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#misc">return to TZP index</a></td></tr> </table> <table id="tb18"> <col width="1%"><col width="8%"><col width="92%"> <thead><tr><th colspan="3"> <div class="nav-title">math spoof fingerprinting <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="3" class="intro"> <span class="no_color">blah blah</span> </td></tr> <tr><td colspan="3"><hr></td></tr> <tr><td colspan="3"><div class="btn-left"><span class="btn18 btn" onClick="outputMath(170)">[ re-run ]</span></div>ANALYSIS</td></tr> <tr><td colspan="3"></td></tr> <!--spacer--> <tr><td colspan="2" class="padr">run 1</td><td class="c mono spaces" id="run1hash"></td></tr> <tr><td colspan="2" class="padr">run 2</td><td class="c mono spaces" id="run2hash"></td></tr> <tr><td colspan="3"></td></tr> <!--spacer--> <tr><td colspan="3"><hr></td></tr> <tr><td colspan="3">[RUN 1] DATA</td></tr> <tr><td></td><td colspan="2" class="c mono spaces" id="output"></td></tr> </table> <br> <script> 'use strict'; /* NOTES: cydec targets: Math.SQRT1_2 Math.PI Math.SQRT2 Math.cosh(1) Math.expm1(1) Math.E Math.LN2 Math.LOG2E Math.LOG10E 1e-310 1e0 */ var fnType = true // toggle between runs function run(runtype) { /* https://en.wikipedia.org/wiki/Trigonometric_functions#cos */ if (runtype == 1) {fnType = !fnType} return new Promise(resolve => { // rebuild it on each run let c = 1 // reference counter let fns = [ // [counter, function1, value1, function2, value2, engine, toFixed] ["separator","hypot/sqrt: known targeted values"], [c++, 'hypot', [Math.PI, Math.PI], 'sqrt', [(Math.PI*Math.PI) + (Math.PI*Math.PI)]], [c++, 'hypot', [Math.cosh(1), Math.cosh(1)], 'sqrt', [(Math.cosh(1)*Math.cosh(1)) + (Math.cosh(1)*Math.cosh(1))]], [c++, 'hypot', [Math.expm1(1), Math.expm1(1)], 'sqrt', [(Math.expm1(1)*Math.expm1(1)) + (Math.expm1(1)*Math.expm1(1))]], [c++, 'hypot', [Math.LOG2E, Math.LOG2E], 'sqrt', [(Math.LOG2E*Math.LOG2E) + (Math.LOG2E*Math.LOG2E)]], [c++, 'hypot', [Math.LOG10E, Math.LOG10E], 'sqrt', [(Math.LOG10E*Math.LOG10E) + (Math.LOG10E*Math.LOG10E)]], [c++, 'hypot', [1e0, 1e0], 'sqrt', [(1e0*1e0) + (1e0*1e0)]], /* in chrome (not sure about Safari) we need to fix up the decimals places 1.0000000000000002 1 2.0000000000000004 2 3.844231028159117 3.8442310281591165 0.9802581434685472 0.9802581434685471 */ [c++, 'hypot', [Math.SQRT1_2, Math.SQRT1_2], 'sqrt', [(Math.SQRT1_2*Math.SQRT1_2) + (Math.SQRT1_2*Math.SQRT1_2)], "nonFF", 15], [c++, 'hypot', [Math.SQRT2, Math.SQRT2], 'sqrt', [(Math.SQRT2*Math.SQRT2) + (Math.SQRT2*Math.SQRT2)], "nonFF", 15], [c++, 'hypot', [Math.E, Math.E], 'sqrt', [(Math.E*Math.E) + (Math.E*Math.E)], "nonFF", 14], [c++, 'hypot', [Math.LN2, Math.LN2], 'sqrt', [(Math.LN2*Math.LN2) + (Math.LN2*Math.LN2)], "nonFF", 15], ["separator", "hypot/sqrt"], [c++, 'hypot', [9, 3], 'sqrt', [(9*9) + (3*3)]], [c++, 'hypot', [5, 7], 'sqrt', [(5*5) + (7*7)]], [c++, 'hypot', [95, -23], 'sqrt', [(95*95) + (-23 * -23)]], [c++, 'hypot', [1e-3, 1e-3, 1e-3], 'none', 0.0017320508075688772], [c++, 'hypot', [1e300, 1e300], 'none', 1.4142135623730952e+300], [c++, 'hypot', [1e100, 1e200, 1e300], 'none', 1e300], [c++, 'hypot', [1e3, 1e-3], 'none', 1000.0000000005], [c++, 'hypot', [1e-300, 1e300], 'none', 1e300], [c++, 'hypot', [1e3, 1e-3, 1e3, 1e-3], 'none', 1414.2135623738021555], [c++, 'hypot', [1e1, 1e2, 1e3], 'sqrt', [1e2 + 1e4 + 1e6]], [c++, 'hypot', [1e1, 1e2, 1e3, 1e4], 'sqrt', [1e2 + 1e4 + 1e6 + 1e8]], // cos/sin ["separator", "cos/sin"], [c++, 'cos', [5-2], 'none', [(Math.cos(5) * Math.cos(2)) + (Math.sin(5) * Math.sin(2))] ], [c++, 'cos', [1-8], 'none', [(Math.cos(1) * Math.cos(8)) + (Math.sin(1) * Math.sin(8))] ], [c++, 'cos', [8-(-1)], 'none', [(Math.cos(8) * Math.cos(-1)) + (Math.sin(8) * Math.sin(-1))] ], // constants ["separator", "constants"], ["indent", "pythagoras SQRT2"], [c++, 'none', ["2.000000000000000"], 'none', [Math.SQRT2 * Math.SQRT2], "all", 15], ["indent", "Theodorus SQRT 3"], [c++, 'none', [3], 'none', [Math.sqrt(3) * Math.sqrt(3)], "all", 15], // law of cosines //["separator", "law of cosines"], //x = 3; y = 3 //let temp = Math.sqrt((x * x) + (y * y) - ( (2 * x * y) * Math.cos(Math.PI/3) )) // limit to 15 decimal places: values is usually 2.9999999999999996 //temp = temp.toFixed(15) //build( temp, 2.999999999999999 ) ] // expected: 26 item: f50756e5747a502327cebeed8b11d8b7fc6a2aa3 let output = [] let data = [] let diffs = [] let detail = [] for(let i=0; i < fns.length; i++) { let fn = fns[i] if (fn[0] == "separator") { // add separator output.push("<br>"+ s18 +" ".repeat(5) + fn[1].toLowerCase() + sc +"<br>") } else if (fn[0] == "indent") { output.push(s12 +" "+ fn[1].toLowerCase() + sc) } else { let r1, r2 try { if (fn[1] == "none") { // just use the value if (Array.isArray(fn[2])) { // in case I left it in an array on it's own let array = fn[2] r1 = array[0] } else { r1 = fn[2] } } else { if (fnType) { r1 = newFn(Math[fn[1]](...fn[2])) //.toString() } else { r1 = (Math[fn[1]](...fn[2])) //.toString() } } } catch(e) { r1 = zErr } try { if (fn[3] == "none") { // just use the value if (Array.isArray(fn[4])) { // in case I left it in an array on it's own let array = fn[4] r2 = array[0] } else { r2 = fn[4] } } else { if (fnType) { r2 = newFn(Math[fn[3]](...fn[4])) //.toString() } else { r2 = (Math[fn[3]](...fn[4])) //.toString() } } } catch(e) { r2 = zErr } // fixed decimal places if (fn[5] !== undefined) { let isFixed = false let fixedType = fn[5] if (fixedType == "nonFF") {if (!isFF) {isFixed = true}} if (fixedType == "all") {isFixed = true} if (isFixed) { let fix = fn[6] try {r1 = r1.toFixed(fix)} catch(e) {} try {r2 = r2.toFixed(fix)} catch(e) {} } } // make results the same type if (typeof r1 === "string") { r2 = r2.toString() } // store pure data data.push([r1,r2]) // compare let notation = "" if (r1 !== zErr && r2 !== zErr) { notation = " "+ (r1 == r2 ? green_tick : red_cross) if (r1 !== r2) { detail.push([fn[0],r1,r2]) diffs.push(fn[0]) } } // pad r1 = r1.toString().padEnd(24) r2 = r2.toString().padEnd(24) // push let ref = fn[0].toString() ref = s12 + ref.padStart(3) + sc +": " output.push(ref + r1 +" "+ r2 + notation) } } // log detailed diffs if (detail.length) { console.log("RUN "+ runtype +" DIFFS: " + detail.length +" item"+ (detail.length > 1 ? "s" : "") +"\n", detail) } if (runtype == 1) { dom.output.innerHTML = output.join("<br>") } return resolve([data,diffs]) }) } function outputMath(delay) { // clear let items = document.getElementsByClassName("c") for (let i=0; i < items.length; i++) {items[i].textContent = ""} // delay so user can see things being reset setTimeout(function(){ let t0 = performance.now() Promise.all([ run(1), run(2), ]).then(function(results){ // run1 let run1 = results[0] let run1hash = mini(run1[0].join()) let run1diff = run1[1] let run1count = run1diff.length run1count = (run1count == 0 ? sg: sb) +"["+ run1count +"]"+ sc + (fnType ? s13 +"[newFn]"+ sc : "") dom.run1hash.innerHTML = run1hash + run1count // run 2 let run2 = results[1] let run2hash = mini(run2[0].join()) let run2diff = run2[1] let run2count = run2diff.length run2count = (run2count == 0 ? sg: sb) +"["+ run2count +"]"+ sc // match hashes let hashmatch = (run1hash == run2hash ? sg : sb) +"[match]"+ sc dom.run2hash.innerHTML = run2hash + run2count + hashmatch // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" }) }, delay) } outputMath(1) </script> </body> </html> ================================================ FILE: tests/newwin.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>i'm a new window</title> <style> body {background-color: #161b22; color: #b3b3b3;} h2 {font-size: 24px} .s1 {background-color: #161b22; color: #dc8c8c;} .hidden {display: none;} </style> </head> <body> <center> <br> <p>feel free to <span class="s1">close</span> me<p> <p><span class="s1">refer</span> back to the<br>results on the main test</p> </center> <br> </body> </html> ================================================ FILE: tests/newwinsim.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <title>newwin sim</title> <!-- custom --> <style> table {width: 780px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#screen">return to TZP index</a></td></tr> </table> <table id="tb1"> <col width="50%"><col width="50%"> <thead><tr><th colspan="2"> <div class="nav-title">new window simulation</div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A "what if" simulation of all possible new window sizes. This is a worst case scenario, and not all variations would be likely, e.g. rediculous scaling or having massive taskbar sizes on low-res displays (I have tried to weed a few of these out). <code>p</code> means portrait e.g. <code>pXGA</code> is XGA in portrait mode.</span> </td></tr> <tr><td colspan="2" class="mono" style="text-align: left; vertical-align: top;"> <span class="btn1 btnfirst" onClick="run('win7')">[ win7 ]</span> <span class="btn1 btn" onClick="run('win11')">[ win11 ]</span> <!--<span class="btn1 btn" onClick="run('mac')">[ mac ]</span>--> | <span class="no_color">lock at default: </span> <input type="checkbox" id="optCompact"> <span class="no_color">compact</span> <input type="checkbox" id="optMenubar"> <span class="no_color">menubar</span> <input type="checkbox" id="optTitlebar"> <span class="no_color">titlebar</span> <br><br><hr> </td></tr> <tr> <td colspan="2" class="mono spaces" style="text-align: left"><span class="no_color" id="results"></span></td> </tr> </table> <br> <script> 'use strict'; // actual resolutions reported (i.e not necessarily native) // https://firefoxgraphics.github.io/telemetry/#view=monitors // https://data.firefox.com/dashboard/hardware // https://gs.statcounter.com/screen-resolution-stats/desktop/worldwide const resCommon = [ // anything smaller than these would have no change with expanding max width from 1000 // statc, FF, dashboard [3840, 2160, "4K UHS-1"], // 1.4% [2880, 1800, "Retina 15in"], // 2.1% [2560, 1440, "WQHD"], // 2.5%, 2.2% [1920, 1200, "WUXGA"], // 0.8%, 1.7% [1920, 1080, "FHD"], // 23.0%, 49.6% [1680, 1050, "WSXGA+"], // 1.4%, 2.2% [1600, 900, "HD+"], // 3.4%, 6.3% [1536, 864], // 10.8%, [1440, 900, "WXSGA"], // 6.0%, 2.6% [1366, 768, "WXGA-HD"], // 16.9%, 21.5% [1360, 768], // 1.0%, 1.3% [1280, 1024, "SXGA"], // 1.8%, 3.2% [1280, 800, "WXGA-max"], // 1.6%, 1.7% [1280, 720, "WXGA-min"], // 6.2% [1024, 768, "XGA"], // 1.6% 2.0% /* ignore for now // none of these change if we up max width from 1000 to 1400 // and none are likely to system scale bigger // p = portrait [ 768, 1024, "pXGA"], // 1.8% // portrait 1.33 [ 810, 1080], // 1.7% // portrait 1.33 [1024, 1366], // 0.7% // portrait 1.33 [ 820, 1180], // 0.6% // portrait 1.43 [ 834, 1194], // 0.5% // portrait 1.43 //*/ // 82.3%, 94.0% // other // 15.9%, 6.1% /* ignored [ 360, 800], // 1.0% [ 800, 600], // 0.9% //*/ ] // these ones we apply scaling to generate more possible // screen resolutions which we scale then merge with common // https://en.wikipedia.org/wiki/List_of_common_resolutions // https://en.wikipedia.org/wiki/Computer_display_standard#Standards // https://en.wikipedia.org/wiki/Graphics_display_resolution#Extended_Graphics_Array_(XGA_and_derivatives) const resNative = [ /* ignore [ 1024, 600], // SVGA 10" netbooks [ 1024, 800], // Sun-1 [ 1080, 1200], // HTC Vive [ 1120, 832], // NeXt [ 1152, 768], // Apple PowerBook G4 [ 1152, 864], // XGA+ (apple) [ 1152, 900], // Sun-2, Sun-3, Sun-4 [ 1280, 854], // Apple PowerBook G4 [ 1440, 960], // Apple PowerBook G4 [ 1440, 1080], // HDV [ 1600, 768], // Sony VAIO P Series (2009-2010) [ 1600, 1024], // SGI [ 1600, 1200], // UXGA: laptops e.g. Lenovo Thankpad T60 (pre 2007) [ 1600, 1280], // Sun-3 [ 2560, 1700], // chromebooks [ 2560, 1920], // max CRT .. WTF? CRT's in this day and age ... good grief [ 8192, 8192], // 8K Fulldome = theatres //*/ /* ignore DCI - video format: digital cinema spec [ 2048, 1080], // DCI 2K [ 4096, 2160], // DCI 4K [ 8192, 4320], // DCI 8K [16384, 8640], // DCI 16K //*/ //* no documentation except "supported in some GPUs, monitors, and games" // don't scale these [ 1152, 720, "1152_720"], [ 1776, 1000, "1776_1000"], [ 1792, 1344, "1792_1344"], [ 1800, 1440, "1800_1440"], [ 1856, 1392, "1856_1392"], [ 2304, 1728, "2304_1728"], [ 2048, 1280, "2048_1280"], [ 2576, 1450, "2576_1450"], [ 2880, 900, "2880_900"], // Alienware //*/ //* temp from resCommon // all the rest of resCommon are below // these maybe == scaled higher res [ 1360, 768, "1360_768"], [ 1536, 864, "1536_864"], //*/ //* temp portrait from resCommon [ 768, 1024, "pXGA"], // 1.8% // portrait 1.33 [ 810, 1080, "p1080_810"], // 1.7% // portrait 1.33 [1024, 1366, "pWXGA-HD"], // 0.7% // portrait 1.33 [ 820, 1180, "p1180_820"], // 0.6% // portrait 1.43 [ 834, 1194, "p1194_834"], // 0.5% // portrait 1.43 //*/ // standards [ 1024, 768, "XGA"], [ 1280, 720, "WXGA-min"], [ 1280, 768, "WXGA"], // WXGA-average [ 1280, 800, "WXGA-max"], [ 1280, 960, "SXGA−"], [ 1280, 1024, "SXGA"], [ 1366, 768, "WXGA-HD"], [ 1440, 900, "WXSGA"], // also WXGA+ [ 1400, 1050, "SXGA+"], [ 1600, 900, "HD+"], [ 1680, 1050, "WSXGA+"], [ 1920, 1080, "FHD"], [ 1920, 1200, "WUXGA"], [ 1920, 1280, "FHD Surface 3"], [ 1920, 1440, "TXGA"], [ 2048, 1152, "QWXGA"], // 2K [ 2160, 1440, "Surface Pro 3"], [ 2304, 1440, "Retina"], [ 2256, 1504, "Surface"], [ 2560, 1080, "UW FHD"], [ 2560, 1440, "WQHD"], [ 2560, 1600, "WQXGA"], [ 2560, 2048, "SQXGA"], [ 2736, 1824, "Surface 4"], // Pro [ 2800, 2100, "QSXGA+"], [ 2880, 1620, "Thinkpad W541"], [ 2880, 1800, "Retina 15in"], // Apple 15" MacBook Pro [ 2880, 1920, "Surface Pro X"], // Pro [ 3000, 2000, "3K"], //MS Surface Book, Huawei MateBook X Pro [ 3072, 1920, "Retina 16in"], // Apple 16" MacBook Pro [ 3200, 1800, "WQXGA+"], [ 3200, 2048, "WQSXGA"], [ 3200, 2400, "QWUXGA"], [ 3240, 2160, "Surface Book 2"], // 15inch [ 3440, 1440, "UWQHD"], [ 3840, 1600, "UW4K"], [ 3840, 2160, "4K UHS-1"], [ 3840, 2400, "WQUXGA"], [ 4096, 2304, "4K Retina"], [ 4096, 3072, "HXGA"], [ 4480, 2520, "4.5K Retina"], [ 4500, 3000, "Surface Studio"], [ 5120, 1440, "DQHD"], [ 5120, 2880, "5K"], [ 5120, 3200, "WHXGA"], [ 5120, 4096, "HSXGA"], [ 6016, 3384, "6K Retina"], [ 6400, 4096, "WHSXGA"], [ 6400, 4800, "HUXGA"], // [ 6480, 3240, "?"], [ 7680, 4320, "8K UDH-2"], [ 7680, 4800, "WHUXGA"], // [ 8192, 4608, "?"], [10240, 4320, "UW10K"], [15360, 8640, "16K"], ] const oResData = { "windows": { // min/max: nothing at compact [1], everything at touch // bookmarks toolbar, menubar, titlebar "chrome": { // devPixels = -1, system scaling 100 "min": [12,85], "max": [16,188], }, "dockerheight": [ // on top/bottom 0, // min: autohide 30, // win 7 single height 48, // win 11 default: can vary by 1 with various scaling 62, // max: win 7 double height ], "dockerwidth": [ // on side 0, // min: autohide 62, // default 130, // max: user stretched it a little to read app titles ], "scaling": [100, 125, 150, 175, 200, 225, 250, 300, 350], // from win7-11: custom scaling 100-500 not recommended } } // VARS let aVariations = [] let oData = {} let oRaw = {} const oTests = { // width, height "ORIGINAL": [1000, 1000], "NEW": [1400, 900], "bigger": [1400, 1000], "embiggened": [1600, 900], "cromulent": [1600, 1000], "X": ["variable up to 1400", 900] } let oBuckets = {} let resUsed = [] let isDimensions = false // show available inner dimensions or use the short basename function calcAB(w, h, origin, basename) { let res = [] for (const k of Object.keys(oTests)) { let maxW = oTests[k][0], maxH = oTests[k][1], finalWidth, finalHeight // height 100's if (maxH < h) { finalHeight = maxH } else { finalHeight = Math.floor(h/100) * 100 } // width 200's // variable steps if (maxW == "variable up to 1400") { if (finalHeight == 900) {maxW = 1400 } else if (finalHeight === 800) {maxW = 1200 } else {maxW = 1000} } if (maxW < w) { finalWidth = maxW } else { finalWidth = Math.floor(w/200) * 200 } res.push([finalWidth, finalHeight, k, origin]) // add to buckets let inner = finalWidth +" x "+ finalHeight if (oBuckets[k][inner] === undefined) { oBuckets[k][inner] = [origin] } else { oBuckets[k][inner].push(origin) } // ToDo: track all the sizes returned per baseName if (oData[k] == undefined) {oData[k] = {}} if (oData[k][basename] == undefined) {oData[k][basename] = []} oData[k][basename].push(inner) } return res } function calc(runtype) { let ignoreWin7DoubleSide = false let ignoreCompact = dom.optCompact.checked, ignoreTitlebar = dom.optTitlebar.checked, ignoreMenubar = dom.optMenubar.checked // build w/h variables let aTemp = [], taskbarheights = [], taskbarwidths = [], menubarheights = [], titlebardata = [], toolbardata = [] let varCountNoDedupe = 0 if (runtype == "win7") { taskbarheights = [62, 30, 0] taskbarwidths = [130, 62] // ignore 0, we already add this calc under height if (ignoreWin7DoubleSide) { taskbarwidths = [62] } menubarheights = [15] // same on all densities titlebardata = [[4, 27]] // [w, h] same on all densities toolbardata = [ [12, 85, 28], // compact: w, h, toolbarheight [12, 98, 28], // normal [12, 107, 34], // touch ] if (ignoreCompact) { toolbardata = [ [12, 98, 28], // normal [12, 107, 34], // touch ] } } else if (runtype == "win11") { taskbarheights = [48, 0] taskbarwidths = [] menubarheights = [28] // same on all densities titlebardata = [[4, 33]] // [w, h] same on all densities toolbardata = [ [12, 78, 28], // compact: w, h, toolbarheight [12, 91, 28], // normal [12, 100, 34], // touch ] if (ignoreCompact) { toolbardata = [ [12, 91, 28], // normal [12, 100, 34], // touch ] } } try { // taskbar taskbarheights.forEach(function(h) { aTemp.push([0, -h]) }) taskbarwidths.forEach(function(w) { aTemp.push([-w, 0]) }) //console.log(aTemp) // menubar if (!ignoreMenubar) { aTemp.forEach(function(item) { menubarheights.forEach(function(h) { aTemp.push([item[0], item[1] - h]) // only one: same per density in windows }) }) //console.log(aTemp) } // titlebar if (!ignoreTitlebar) { aTemp.forEach(function(item) { titlebardata.forEach(function(pair) { aTemp.push([item[0] - pair[0], item[1] - pair[1]]) // only one: same per density in windows }) }) //console.log(aTemp) } // chrome w/ + w/o toolbar at each density for (let i = 0; i < aTemp.length; i++) { let w0 = aTemp[i][0], h0 = aTemp[i][1] toolbardata.forEach(function(array) { let wChrome = array[0], hChrome = array[1], tChrome = array[2] // no toolbar aVariations.push([w0 - wChrome, h0 - hChrome]) // toolbar aVariations.push([w0 - wChrome, h0 - (hChrome + tChrome)]) }) } } catch(e) { console.log(e) } // dedupe: e.g. 120 in win7 but only 96 are unique let aUnique = [] aVariations.forEach(function(array) { aUnique.push(array.join("x")) }) aUnique.sort() varCountNoDedupe = aUnique.length aUnique = aUnique.filter(function(item, position) {return aUnique.indexOf(item) === position}) aVariations = [] aUnique.forEach(function(item) { let w = item.split("x")[0], h = item.split("x")[1] aVariations.push([w * 1, h * 1]) }) //console.log(aVariations.join("\n")) // win7 variations that include double height taskbar // these are unique so we don't need to check width let aIgnoreHeights = [ -147, -160, -162, -169, -175, -184, -188, -190, -203, -218, // @ -12 width -174, -187, -189, -196, -202, -211, -215, -217, -230, -245, // @ -16 width ] let aIgnoreWidths = [-142, -146] // win7 variations that include expanded taskbar on side let nameCheck = [] resUsed.forEach(function(item) { try { let baseWidth = item[0], baseHeight = item[1] let skipHeight = baseHeight < 800, skipWidth = baseWidth < 1000 let baseName = item[2] !== undefined ? item[2] : baseWidth +" x "+ baseHeight let originName = item[2] !== undefined ? item[2] +" " : "" nameCheck.push(baseName) oRaw["resBase"][baseName] = [] let AB = [] aVariations.forEach(function(item) { let go = true if (runtype == "win7") { if (skipHeight) { if (aIgnoreWidths.includes(item[0])) {go = false} } if (go && skipHeight) { if (aIgnoreHeights.includes(item[1])) {go = false} } } if (go) { let w = item[0], h = item[1] let availableWidth = baseWidth + w, availableHeight = baseHeight + h let nameAvailable = availableWidth +" x "+ availableHeight oRaw["resAvailable"].push(nameAvailable) let nameAvailableInner = (isDimensions ? originName + availableWidth +" x "+ availableHeight : baseName) oRaw["innerAvailable"].push(nameAvailableInner) AB = calcAB(availableWidth, availableHeight, nameAvailableInner, baseName) } }) } catch(e) { console.log(e) } }) nameCheck.sort() //console.log(nameCheck) let intInner = oRaw["innerAvailable"].length let display = [s3 + runtype.toUpperCase() + sc +" | " + s3 + resUsed.length + sc + " base res | " + s3 + aVariations.length + sc + " unique from "+ s3 + varCountNoDedupe + sc + " chrome/os variations | " + s3 + intInner + sc + " tests per scenario<br>" ] display.push(s3 +"compact mode: "+ sc + (ignoreCompact ? "disabled" : "allowed") + s3 +" | menubar: "+ sc + (ignoreMenubar ? "default off" : "allowed") + s3 +" | titlebar: "+ sc + (ignoreTitlebar ? "default off" : "allowed") ) for (const k of Object.keys(oTests)) { let data = oBuckets[k] let bucketcount = Object.keys(data).length let testname = oTests[k][0] +" x " + (oTests[k][1]+"").padStart(4, " ") let detail = [] for (const j of Object.keys(oBuckets[k]).sort()) { let array = oBuckets[k][j].sort() let count = array.length if (!isDimensions) { // lets dedupe the basenames in each bucket with a count if > 1 let tmpObj = {}, newArray = [] array.forEach(function(base) { if (tmpObj[base] == undefined) {tmpObj[base] = 1} else {tmpObj[base] = tmpObj[base] + 1} }) for (const m of Object.keys(tmpObj)) { let note = (tmpObj[m] > 1) ? s99 +" ["+ tmpObj[m] +"]"+ sc : "" newArray.push(m + note) } array = newArray } detail.push(s3 + j + sc + " [" + count +"] " + "<ul>" + array.join(", ") +"</ul>") } display.push("<br>"+ s1 + k +": "+ sc + testname + s1 +" [" + (bucketcount+"").padStart(2, " ") +" buckets]" + sc +"<br><details><summary>details</summary>" + detail.join("<br>") + "</details>" ) console.log(k +": "+ testname +" [" + bucketcount +" buckets]\n", data) //if (k == "B" || k == "E") { // console.log(k +": "+ testname +" [" + bucketcount +" buckets]\n") //} } dom.results.innerHTML = display.join("<br>") // cleanup buckets per res base: oData for (const k of Object.keys(oData)) { for (const j of Object.keys(oData[k])) { let tmpArray = oData[k][j] tmpArray = tmpArray.filter(function(item, position) {return tmpArray.indexOf(item) === position}) tmpArray.sort() oData[k][j] = tmpArray } } //console.log(oData) //console.log(oRaw) } function run(runtype) { // reset display dom.results.innerHTML = "" // reset vars aVariations = [] oData = {} for (const k of Object.keys(oTests)) { oBuckets[k] = {} } oRaw = { "resBase": {}, "resAvailable": [], "innerAvailable": [], } // ToDo select res array to use //resUsed = resCommon resUsed = resNative setTimeout(function() { calc(runtype) }, 170) // delay so user can see it's working } </script> </body> </html> ================================================ FILE: tests/nfcompact.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>nf: compactdisplay</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2"> <div class="nav-title">numberformat : compactdisplay <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy in compactDisplay and useGrouping</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], isBigIntSupported = false, localesHashAll = "" // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = Intl.NumberFormat.supportedLocalesOf([code]).length > 0 if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat // Note: ignore ChainNumberFormat (FF54) ? not sure how to use it TBH let grouping = ["true", "false"] let styles = ["short", "long"] let items = [ 0/0, 0, 1/10, 200, // hundred 1000, // thousand 2000000, // million -1100000000, // billion 6600000000000, // trillion 7000000000000000, // force a group Infinity, /* // there seems to be no change in plurality (million vs millions), but // using certain leading digits affects entropy: test from time to time 100, 200, 300, 400, 500, 600, 700, 800, 900, // hundred(s) 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, // thousand(s) 1000000, 2000000, 3000000, 4000000, 5000000, 6000000, 7000000, 8000000, 9000000,// million(s) // billion(s) 1000000000, 2000000000, 3000000000, 4000000000, 5000000000, 6000000000, 7000000000, 8000000000, 9000000000, // trillion(s) 1000000000000, 2000000000000, 3000000000000, 4000000000000, 5000000000000, 6000000000000, 7000000000000, 8000000000000, 9000000000000, // force a group 1000000000000000, 2000000000000000, 3000000000000000, 4000000000000000, 5000000000000000, 6000000000000000, 7000000000000000, 8000000000000000, 9000000000000000, //*/ ] if (isBigIntSupported) { items.push( BigInt("987354000000000000") ) // add a bigint } let tests = { "false": { "long": items, "short": items, }, "true": { "long": items, "short": items, }, } if (method == "min") { tests = { // FF78+ "true": { "long": [0/0, 1000, 2e6, 6.6e12, 7e15], // force group on trillions "short": [ -1100000000, // only needed for FF133 or lower -1000, // FF134+ ], } } // FF77 or lower: add 1/10 if (isFF && !window.Document.prototype.hasOwnProperty("replaceChildren")) { tests["true"]["long"] = [0/0, 1/10, 1000, 2e6, 6.6e12, 7e15] } if (isBigIntSupported) { tests["true"]["long"].push( BigInt("987354000000000000") ) // we need the bigint } } try { //test: each useGrouping x each compactDisplay aLocales.forEach(function(code) { let oStyles = {} Object.keys(tests).sort().forEach(function(g){ oStyles[g] = {} Object.keys(tests[g]).sort().forEach(function(s){ oStyles[g][s] = [] // set formatter once per code let gValue = g == "true" ? true : false let formatter = new Intl.NumberFormat(code, {notation: "compact", compactDisplay: s, useGrouping: gValue }) tests[g][s].forEach(function(t){ oStyles[g][s].push(formatter.format(t)) }) }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(tests).sort().forEach(function(g){ oTempData[hash][g] = {} Object.keys(tests[g]).sort().forEach(function(s){ oTempData[hash][g][s] = oStyles[g][s].join(" | ") }) }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == "locales") { oData[n][p] = oTempData[n][p].join(", ") } else { oData[n][p] = oTempData[n][p] } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length let strFL = "", strFS = "", strFalse = "" if (oData[k]["false"] !== undefined) { if (oData[k]["false"]["long"] !== undefined) {strFL = "<li>"+ s16 +"L: "+ sc + oData[k]["false"]["long"] +"</li>"} if (oData[k]["false"]["short"] !== undefined) {strFS = "<li>"+ s16 +"S: "+ sc + oData[k]["false"]["short"] +"</li>"} strFalse = s14 +"false"+ sc + strFS + strFL } let strTL = "", strTS = "", strTrue = "" if (oData[k]["true"] !== undefined) { if (oData[k]["true"]["long"] !== undefined) {strTL = "<li>"+ s16 +"L: "+ sc + oData[k]["true"]["long"] +"</li>"} if (oData[k]["true"]["short"] !== undefined) {strTS = "<li>"+ s16 +"S: "+ sc + oData[k]["true"]["short"] +"</li>"} strTrue = s14 +"true"+ sc + strTS + strTL } displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ strFalse + strTrue + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "c4044acb") { // FF151+ } else if (resultsHash == "c00615d3") { // FF147-150 } else if (resultsHash == "bc3aadde") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "b8fa8ded") { // FF151+: 179 } else if (localesHash == "3220ea11") { // FF147-150: 178 } else if (localesHash == "6141ce3e") { // FF140-146: 170 } else if (isFF) {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // add additional locales to core locales for this test let aListExtra = [ 'ar-bh,arabic (bahrain)', 'ar-dz,arabic (algeria)', 'bs-cyrl,bosnian (cyrillic)', 'de-at,german (austria)', 'de-ch,german (switzerland)', 'en-at,english (austria)', 'en-ch,english (switzerland)', 'en-gb,english (united kingdom)', 'en-fi,english (finland)', 'en-in,english (india)', 'es-cr,spanish (costa rica)', 'es-mx,spanish (mexico)', 'es-py,spanish (paraguay)', 'es-us,spanish (united states)', 'ff-adlm,fulah (adlam)', 'fr-ca,french (canada)', 'fr-ch,french (switzerland)', 'fr-lu,french (luxembourg)', 'it-ch,italian (switzerland)', 'kk-cn,kazakh (china)', 'kok-latn,konkani (latin)', 'kxv-telu,kuvi (telugu)', 'ms-bn,malay (brunei)', 'pt-pt,portuguese (portugal)', 'se-fi,northern sami (finland)', 'sr-latn,serbian (latin)', 'sw-cd,swahili (congo kinshasa)', 'sw-ke,swahili (kenya)', 'ta-my,tamil (malaysia)', 'ur-in,urdu (india)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'yo-bj,yoruba (benin)', 'yue-hans,cantonese (simplified)', // blink 'az-cyrl,azerbaijani (cyrillic)', 'pa-arab,punjabi (arabic)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() // check bigint support: FF68+ try { let y = BigInt('9999999999999999') isBigIntSupported = true } catch(e) {} setBtn('all') setTimeout(function() { run_main('all') }, 100) }) </script> </body> </html> ================================================ FILE: tests/nfcurrency.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>nf: currencydisplay</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 680px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"><col> <thead><tr><th colspan="2"> <div class="nav-title">numberformat: currencydisplay <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy in currencyDisplay and currencySign. The <code>TINY</code> test is a minimalist hardcoded test built for speed. </span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <span id="btiny" class="btn4 btn" onClick="run('tiny')">[ TINY ]</span> <span id="allcurrency"><input type="checkbox" id="optCurrency"> +cur</span> <br><br><hr><br> <span class="spaces" id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var oCurrencies = {}, list = gLocales, aLegend = [], aLocales = [], isSupported = false, isBigIntSupported = false, localesHashAll = '' // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "currencies") { console.log(name +": " + sDetail[name].length +"\n"+ sDetail[name].join(", ")) } else if (name == "allcurrencies") { console.log("all supported currencies" +": " + sDetail[name].length +"\n"+ sDetail[name].join(", ")) } else if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail[name].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.NumberFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function set_currency_lists() { let supported = [] try { supported = Intl.supportedValuesOf("currency") } catch(e) {} supported.sort() oCurrencies["supported"] = supported // expanded: for additonal locale tests let expanded = [ 'AED','ANG','AOA','ARS','AUD','AWG','AZN','BAM','BBD','BIF','BMD','BOB', 'BRL','BSD','BWP','BYN','BZD','CAD','CDF','CNY','COP','CUP','DJF','DKK', 'DZD','ERN','ETB','FJD','FKP','FRF','GBP','GEL','GTQ','GHS','GIP','GMD', 'GNF','GYD','HNL','HTG','JMD','KES','KGS','KMF','KYD','KZT','LKR','LRD', 'LSL','LUF','MDL','MGA','MKD','MOP','MRU','MUR','MWK','MYR','MZN','NAD', 'NGN','NIO','NOK','NZD','PAB','PGK','PHP','PKR','PLN','RWF','SBD','SCR', 'SDG','SGD','SHP','SLE','SOS','SRD','SSP','STN','SYP','SZL','TND','TOP', 'TTD','TZS','XCD','UGX','USD','VUV','WST','XAF','XXX','ZAR','ZMW', // blink 'PEN','PYG', ] if (supported.length) {expanded = expanded.filter(x => supported.includes(x))} expanded = expanded.filter(function(item, position) {return expanded.indexOf(item) === position}) expanded.sort() oCurrencies["expanded"] = expanded // ignore: don't seem to add any more buckets let ignore = [ 'ADP','AFA','AFN','ALK','ALL','AMD','AOK','AON','AOR','ARA','ARL','ARM', 'ARP','ATS','AZM','BAD','BAN','BDT','BEC','BEF','BEL','BGL','BGM','BGN', 'BGO','BHD','BND','BOL','BOP','BOV','BRB','BRC','BRE','BRN','BRR','BRZ', 'BTN','BUK','BYB','BYR','CHE','CHF','CHW','CLE','CLF','CLP','CNH','CNX', 'COU','CRC','CSD','CSK','CUC','CVE','CYP','CZK','DDM','DEM','DOP','ECS', 'ECV','EEK','EGP','ESA','ESB','ESP','EUR','FIM','GEK','GHC','GNS','GQE', 'GRD','GWE','GWP','HKD','HRD','HRK','HUF','IDR','IEP','ILP','ILR','ILS', 'INR','IQD','IRR','ISJ','ISK','ITL','JOD','JPY','KHR','KPW','KRH','KRO', 'KRW','KWD','LAK','LBP','LTL','LTT','LUC','LUL','LVL','LVR','LYD','MAD', 'MAF','MCF','MDC','MGF','MKN','MLF','MMK','MNT','MRO','MTL','MTP','MVP', 'MVR','MXN','MXP','MXV','MZE','MZM','NIC','NLG','NPR','OMR','PEI','PES', 'PLZ','PTE','QAR','RHD','ROL','RON','RSD','RUB','RUR','SAR','SDD','SDP', 'SEK','SIT','SKK','SLL','SRG','STD','SUR','SVC','THB','TJR','TJS','TMM', 'TMT','TPE','TRL','TRY','TWD','UAH','UAK','UGS','USN','USS','UYI','UYP', 'UYU','UYW','UZS','VEB','VED','VEF','VES','VND','VNN','XAG','XAU','XBA', 'XBB','XBC','XBD','XDR','XEU','XFO','XFU','XOF','XPD','XPF','XPT','XRE', 'XSU','XTS','XUA','YDD','YER','YUD','YUM','YUN','YUR','ZAL','ZMK','ZRN', 'ZRZ','ZWD','ZWL','ZWR', ] let all = expanded.concat(ignore) // so at least we have soomething if supported = empty all = all.concat(supported) // in case anything new turns up all = all.filter(function(item, position) {return all.indexOf(item) === position}) if (supported.length) {all = all.filter(x => supported.includes(x))} all.sort() oCurrencies["all"] = all } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" let optCurrency = dom.optCurrency.checked let intTests = 0 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat let tests = {}, tmptests = [] let items = { // these do not add anything // "code", "narrowSymbol", or the values 0/0, 1000, 1000000 "name": [-1], "symbol": [1000], } let curAll = {"accounting": [-1000], "name": [-1], "symbol": [1000]}, curA = {"accounting": [-1000]}, curAN = {"accounting": [-1000], "name": [-1]}, curAS = {"accounting": [-1000], "symbol": [1000]}, curN = {"name": [-1]}, curNS = {"name": [-1], "symbol": [1000]}, curS = {"symbol": [1000]} if (method == "all") { let currencies = [] currencies = oCurrencies["expanded"] if (optCurrency) {currencies = oCurrencies["all"]} currencies.forEach(function(c){ /* curAll : 426 --- curA : 341 curAN : 426 <- curAS : 344 curN : 259 curNS : 426 <- curS : 325 */ tests[c] = curNS //curAll }) intTests = currencies.length * Object.keys(curNS).length // style then currency isn't any faster // 2.3s = old / 2.2 new } else if (method == "tiny") { tests = { "USD": curAN, // 264 "XXX": curN, // 275 (+11) 'ETB': curN, // 281 (+6) "GBP": curS, // 286 (+5) "KES": curS, // 291 (+5) } } else if (method == "min") { // looking for 426 tests = { // from tiny "USD": curAN, // 264 "XXX": curNS, // 275 (+11) // added S 'ETB': curNS, // 281 (+6) // added S "GBP": curNS, // 286 (+5) // added N "KES": curS, // 291 (+5) // everything else now with NS = 426 max // 294 so far 'GHS': curNS, 'MOP': curNS, // +4s = 302 // +3s 'ANG': curNS, 'BYN': curNS, 'DJF': curNS, 'ERN': curNS, 'MRU': curNS, 'GNF': curNS, 'LUF': curNS, 'PKR': curNS, 'RWF': curNS, // 329 // +2s 'AUD': curNS, 'BIF': curNS, 'BWP': curNS, 'BZD': curNS, 'DKK': curNS, 'GMD': curNS, 'KMF': curNS, 'LRD': curNS, 'MDL': curNS, 'MGA': curNS, 'MUR': curNS, 'MZN': curNS, 'NAD': curNS, 'NGN': curNS, 'SCR': curNS, 'SGD': curNS, 'SLE': curNS, 'SZL': curNS, 'TZS': curNS, 'UGX': curNS, 'VUV': curNS, 'ZAR': curNS, // 373 // +1s name 'CAD': curN, 'XAF': curN, // 375 // +1s symbol 'AOA': curS, 'ARS': curS, 'AWG': curS, 'AZN': curS, 'BAM': curS, 'BBD': curS, 'BMD': curS, 'BRL': curS, 'BSD': curS, 'CDF': curS, 'CNY': curS, 'DZD': curS, 'FJD': curS, 'FKP': curS, 'FRF': curS, 'GEL': curS, 'GTQ': curS, 'GIP': curS, 'GYD': curS, 'HNL': curS, 'HTG': curS, 'JMD': curS, 'KGS': curS, 'KYD': curS, 'KZT': curS, 'LKR': curS, 'LSL': curS, 'MKD': curS, 'MWK': curS, 'MYR': curS, 'NIO': curS, 'NOK': curS, 'NZD': curS, 'PAB': curS, 'PGK': curS, 'PHP': curS, 'PLN': curS, 'SBD': curS, 'SDG': curS, 'SHP': curS, 'SOS': curS, 'SRD': curS, 'SSP': curS, 'STN': curS, 'SYP': curS, 'TND': curS, 'TOP': curS, 'TTD': curS, 'XCD': curS, 'WST': curS, 'ZMW': curS, // +51 = 426 // blink 'PEN': curS, // just for blink } } if ('all' !== method) { for (const k of Object.keys(tests)) {intTests += Object.keys(tests[k]).length} } // remove unsupported if min: the others have aleady been checked if (method == 'min') { let supported = oCurrencies["supported"] if (supported.length) { for (const k of Object.keys(tests)) { if (!supported.includes(k)) {delete tests[k]} } } } let oStyles = {} try { aLocales.forEach(function(code) { // for each locale oStyles = {} Object.keys(tests).sort().forEach(function(c){ // for each currency: sort for consistency oStyles[c] = {} Object.keys(tests[c]).forEach(function(cd){ // for each currencyDisplay oStyles[c][cd] = [] try { let option = {style: "currency", currency: c, currencyDisplay: cd} if (cd == "accounting") {option = {style: "currency", currency: c, currencySign: cd}} // accounting is currencySign let formatter = Intl.NumberFormat(code, option) tests[c][cd].forEach(function(n){ // for each number oStyles[c][cd].push(formatter.format(n)) }) } catch (e) {} // ignore invalid }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oStyles).forEach(function(c){ // each currency oTempData[hash][c] = {} Object.keys(oStyles[c]).forEach(function(cd){ // for each currencyDisplay if (oStyles[c][cd].length) { oTempData[hash][c][cd] = oStyles[c][cd].join(" | ") } }) }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const c of Object.keys(oTempData[h]).sort()) { // for each currency if (c == "locales") { oData[h][c] = oTempData[h][c].join(", ") } else { if (Object.keys(oTempData[h][c]).length) { oData[h][c] = oTempData[h][c] } } } } let localeGroups = [], displaylist = [] for (const h of Object.keys(oData)) { // for each hash localeGroups.push(oData[h]["locales"]) let localeCount = oData[h]["locales"].split(",").length let str = "" for (const c of Object.keys(oData[h])) { // for each currency if (c !== "locales") { str += "<li>"+ s12 + c + ":"+ sc Object.keys(oData[h][c]).forEach(function(cd){ // for each currencyDisplay let option = ('all' == method) ? cd.slice(0,1).toUpperCase() : cd str += ' '+ s16 + option +": "+ sc + oData[h][c][cd] }) str += "</li>" } } // wrap into details for long lists if (Object.keys(tests).length > 15) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + h + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[h]["locales"] +"</li></ul>" ) } // hashes + btns sDetail["currencies"] = Object.keys(tests).sort() let curStr = Object.keys(tests).length, curBtn = "" if (curStr < 6) { let aCur = [] for (const k of Object.keys(tests)) {aCur.push(k)} curBtn = aCur.join(", ") + s4 +" ["+ curStr +"]"+ sc } else { curBtn = "<span class='btn4 btnc' onClick='log_console(`currencies`)'>[" + curStr + "]</span>" sDetail["results"] = oData } if (oCurrencies["supported"].length) { if (!optCurrency || optCurrency && method == "min") { sDetail["allcurrencies"] = oCurrencies["all"] curBtn += " from "+ "<span class='btn4 btnc' onClick='log_console(`allcurrencies`)'>[" + oCurrencies["supported"].length + "]</span>" } } let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* //console.log(localesHash) //console.log(localeGroups) //console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all" && !optCurrency) { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "975e37a5") { // FF151+ } else if (resultsHash == "8927863e") { // FF147-150 } else if (resultsHash == "105215e4") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "f3b23b8f") { // FF147+: 430 } else if (localesHash == "e4c818f6") { // FF140-146: 426 } else if (isFF) {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"] " + sc + spacer + s12 +"currencies: "+ sc + curBtn +' ['+ intTests + ' tests]'+spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = '' let status = '' if ('tiny' !== method) { status = 'calculating ...'+ ('all' == method ? ' takes a few seconds': '') } dom.results = status // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() dom.optCurrency.checked = false if (!isFile) { dom.allcurrency.style.display = "none" } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat // note: numberformat supported since at least FF29 // FF78+ : currencyDisplay ? but it works all the way back to FF52! ?!^$%? try { // pointless if we can't use the feature being tested: FF78+ let test = new Intl.NumberFormat("en", {style: "currency", currency: "USD", currencyDisplay: "symbol"}).format(5) test += ", "+ new Intl.NumberFormat("en", {style: "currency", currency: "USD", currencyDisplay: "name"}).format(5) // FF52: USD5.00, $5.00, 5.00 US dollars // FF111: USD 5.00, $5.00, 5.00 US dollars // console.log(test) isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // check bigint support: FF68+ try { let y = BigInt("9999999999999999") isBigIntSupported = true } catch(e) {} // add additional locales to core locales for this test let aListExtra = [ "af-na,afrikaans (namibia)", "ar-ae,arabic (united arabic emirates)", "ar-bh,arabic (bahrain)", "ar-dj,arabic (djibouti)", "ar-dz,arabic (algeria)", "ar-er,arabic (eritrea)", "ar-km,arabic (cosmoros)", "ar-lb,arabic (lebanon)", "ar-so,arabic (somalia)", "ar-ss,arabic (south sudan)", "az-cyrl,azerbaijani (cyrillic)", "bn-in,bengali (india)", "bo-in,tibetan (india)", "bs-cyrl,bosnian (cyrillic)", "ca-fr,catalan (france)", "de-at,german (austria)", "de-ch,german (switzerland)", "de-li,german (liechtenstein)", "de-lu,german (luxembourg)", "en-150,english (europe)", "en-ag,english (antigua & barbuda)", "en-at,english (austria)", "en-au,english (australia)", "en-bb,english (barbados)", "en-bi,english (burundi)", "en-bm,english (bermuda)", "en-bs,english (bahamas)", "en-bw,english (botswana)", "en-bz,english (belize)", "en-ca,english (canada)", "en-cc,english (cocos islands)", "en-ch,english (switzerland)", "en-dk,english (denmark)", "en-er,english (eritrea)", "en-fi,english (finland)", "en-fj,english (fiji)", "en-fk,english (falkland islands)", "en-gb,english (united kingdom)", "en-gg,english (guernsey)", "en-gh,english (ghana)", "en-gi,english (gibraltar)", "en-gm,english (gambia)", "en-gy,english (guyana)", "en-in,english (india)", "en-jm,english (jamaica)", "en-ke,english (kenya)", "en-ky,english (cayman islands)", "en-lr,english (liberia)", "en-ls,english (lesotho)", "en-mg,english (madagascar)", "en-mo,english (macau)", "en-mt,english (malta)", "en-mu,english (mauritius)", "en-mw,english (malawai)", "en-my,english (malaysia)", "en-na,english (namibia)", "en-ng,english (nigeria)", "en-nz,english (new zealand)", "en-pg,english (papua new guinea)", "en-pk,english (pakistan)", "en-rw,english (rwanda)", "en-sb,english (solomon islands)", "en-sc,english (seychelles)", "en-se,english (sweden)", "en-sg,english (singapore)", "en-sh,english (saint helena)", "en-sl,english (sierra leone)", "en-ss,english (south sudan)", "en-sx,english (sint maarten)", "en-sz,english (swaziland)", "en-to,english (tonga)", "en-tt,english (trinidad & tobago)", "en-tz,english (tanzania)", "en-ug,english (uganda)", "en-vu,english (vanuatu)", "en-ws,english (samoa)", "en-za,english (south africa)", "en-zm,english (zambia)", "es-419,spanish (latin america and the caribbean)", "es-ar,spanish (argentina)", "es-bo,spanish (bolivia)", "es-br,spanish (brazil)", "es-bz,spanish (belize)", "es-cl,spanish (chile)", "es-co,spanish (colombia)", "es-cr,spanish (costa rica)", "es-cu,spanish (cuba)", "es-do,spanish (dominican republic)", "es-ec,spanish (ecuador)", "es-gq,spanish (equatorial guinea)", "es-gt,spanish (guatemala)", "es-hn,spanish (honduras)", "es-mx,spanish (mexico)", "es-ni,spanish (nicaragua)", "es-pa,spanish (panama)", "es-pe,spanish (peru)", "es-ph,spanish (philippines)", "es-py,spanish (paraguay)", "es-sv,spanish (el salvador)", "es-us,spanish (united states)", "es-uy,spanish (uruguay)", "es-ve,spanish (venezuela)", "fa-af,persian (afghanistan)", "ff-adlm,fulah (adlam)", "ff-adlm-bf,fulah (adlam burkina faso)", "ff-adlm-gh,fulah (adlam ghana)", "ff-adlm-gm,fulah (adlam gambia)", "ff-adlm-lr,fulah (adlamd liberia)", "ff-adlm-mr,fulah (adlam mauritania)", "ff-adlm-ng,fulah (adlam nigeria)", "ff-adlm-sl,fulah (adlam sierra leone)", "ff-gn,fulah (guinea)", "ff-mr,fulah (mauritania)", "fo-dk,faroese (denmark)", "fr-bi,french (burundi)", "fr-ca,french (canada)", "fr-cd,french (congo kinshasa)", "fr-ch,french (switzerland)", "fr-dj,french (djibouti)", "fr-dz,french (algeria)", "fr-gn,french (guinea)", "fr-ht,french (haiti)", "fr-km,french (comoros)", "fr-lu,french (luxembourg)", "fr-ma,french (morocco)", "fr-mg,french (madagascar)", "fr-mr,french (mauritania)", "fr-mu,french (mauritius)", "fr-rw,french (rwanda)", "fr-sc,french (seychelles)", "fr-sy,french (syria)", "fr-tn,french (tunisia)", "fr-vu,french (vanuatu)", "ha-gh,hausa (ghana)", "hi-latn,hindi (latin)", "hr-ba,croatian (bosnia & herzegovina)", "it-ch,italian (switzerland)", "kea-cv,kabuverdianu (cape verde)", 'kk-cn,kazakh (china)', "ks-deva,kashmiri (devanagari)", "kxv-telu,kuvi (telugu)", "ln-ao,lingala (angola)", "mas-tz,masia (tanzania)", "ms-bn,malay (brunei)", "ms-id,malay (indonesia)", "ms-sg,malay (singapore)", "nl-aw,dutch (aruba)", "nl-bq,dutch (caribbean netherlands)", "nl-cw,dutch (curaçao)", "nl-sr,dutch (suriname)", "om-ke,oromo (kenya)", "os-ru,ossetian (russia)", "pa-pk,punjabi (pakistan)", "ps-pk,pashto (pakistan)", "pt-ao,portuguese (angola)", "pt-ch,portuguese (switzerland)", "pt-cv,portuguese (cape verde)", "pt-lu,portuguese (luxembourg)", "pt-mo,portuguese (macau)", "pt-mz,portuguese (mazambique)", "pt-pt,portuguese (portugal)", "pt-st,portuguese (são tomé & príncipe)", "qu-bo,quechua (bolivia)", "qu-ec,quechua (ecuador)", "ro-md,romanian (moldova)", "ru-by,russian (belarus)", "ru-kg,russian (kyrgyzstan)", "ru-kz,russian (kazakhstan)", "ru-md,russian (moldova)", "ru-ua,russian (ukraine)", "sd-deva,sindhi (devanagari)", "se-se,northern sami", "shi-latn,tachelhit (latin)", "so-dj,somali (djibouti)", "so-et,somali (ethiopia)", "so-ke,somali (kenya)", "sq-mk,albanian (macedonia)", "sr-cyrl-ba,serbian (cyrillic bosnia & herzegovina)", "sr-latn,serbian (latin)", "sr-latn-ba,serbian (latin bosnia & herzegovina)", 'st-ls,southern sotho', "sw-cd,swahili (congo kinshasa)", "sw-ke,swahili (kenya)", "sw-ug,swahili (uganda)", "ta-lk,tamil (sri lanka)", "ta-my,tamil (malaysia)", "ta-sg,tamil (singapore)", "teo-ke,teso (kenya)", "ti-er,tigrinya (eritrea)", 'tn-bw,tswana (botswana)', "tr-tr,turkish (turkey)", "ur-in,urdu (india)", "uz-af,uzbek (afghanistan)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "vai-latn,vai (latin)", "yo-bj,yoruba (benin)", "yue-hans,cantonese (simplified)", "zh-hans-hk,chinese (simplified hong kong)", "zh-hans-mo,chinese (simplified macau)", "zh-hant-mo,chinese (traditional macau)", ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en','de','sv'] legend() set_currency_lists() if (isSupported) { setBtn("all") dom.results = "calculating ... takes a few seconds" setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/nfformattoparts.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>nf: number formattoparts</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 580px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">numberformat: formattoparts <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">testing values, not entropy</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], oTestData = {}, isSupported = false, isBigIntSupported = false let aNumsys = [ 'undefined', // 58 // the rest vary 51-53 except for arab/arabext and 15 and 17 'adlm','ahom','arab','arabext','bali','beng','bhks','brah','cakm','cham','deva','diak','fullwide','gara','gong', 'gonm','gujr','gukh','guru','hanidec','hmng','hmnp','java','kali','kawi','khmr','knda','krai','lana','lanatham', 'laoo','latn','lepc','limb','mathbold','mathdbl','mathmono','mathsanb','mathsans','mlym','modi','mong','mroo', 'mtei','mymr','mymrepka','mymrpao','mymrshan','mymrtlng','nagm','newa','nkoo','olck','onao','orya','osma','outlined', 'rohg','saur','segment','shrd','sind','sinh','sora','sund','sunu','takr','talu','tamldec','telu','thai','tibt', 'tirh','tnsa','vaii','wara','wcho', ] aNumsys.sort() function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.NumberFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function testitems() { //loop each numsys on it's own dom.perf = "" dom.results = "" oTestData = {} setTimeout(function() { try { aNumsys.forEach(function(item){ // allow testing arrays of scripts let testarray = 'object' == typeof item ? item : [item] run_main(item, true) }) } catch(e) { console.log(e) } }, 5) } function run_main(numsys = undefined, isLoop = false) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" if (!isLoop) { dom.perf = "" dom.results = "" } if (numsys == 'undefined') {numsys = undefined} let nBig if (isBigIntSupported) { nBig = BigInt("987354000000000000") } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options // ^ options // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts#return_value // ^ return values //"exponentMinusSign" returns minusSign as type let tests = { // these need no options "decimal": [1.2], "group": [1000, 99999], "infinity": [Infinity], "minusSign": [-5], "nan": ["a"], // these items require options which means another constructor //"percentSign": [.2], // {style: "percent"} //"exponentSeparator": [1], //{style: 'scientitic'} /* already captured elsewhere - units + currency: both are large and lots of constructors - literals - extra constructors - percentSign + exponentSeparator complicates TZP tests due to changein constuctor */ } function get_value(type, parts) { try { let str = "none" for (let i = 0 ; i < parts.length; i++) { if (parts[i]["type"] === type) { str = parts[i]["value"] str += str.length == 1 ? " ("+ str.charCodeAt(0) +")" : "" return str // no need to keep checking } } return str } catch(e) { return "error" } } try { aLocales.forEach(function(code) { let formatter = new Intl.NumberFormat(code, {numberingSystem: numsys}) let oStyles = {} Object.keys(tests).forEach(function(t){ // each type - DO NOT SORT // we need to ensure we have the right options // we can do this by grouping by style, then by the test name // but for now we don't know how much we want to add, so for now // lets just add items after default and change the formatter as required if ('percentSign' == t) { formatter = new Intl.NumberFormat(code, {style: 'percent', numberingSystem: numsys}) } else if ('exponentSeparator' == t) { formatter = new Intl.NumberFormat(code, {notation: 'scientific', numberingSystem: numsys}) } oStyles[t] = [] tests[t].forEach(function(n){ // each number let value = get_value(t, formatter.formatToParts(n)) if (list.length == 1) { console.log(t,formatter.formatToParts(n)) } oStyles[t].push(value) }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(tests).forEach(function(s){ oTempData[hash][s] = oStyles[s].join(" | ") }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = "aww snap, something went wrong" return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == "locales") { oData[n][p] = oTempData[n][p].join(", ") } else { oData[n][p] = oTempData[n][p] } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length if (!isLoop) { let str = "" for (const p of Object.keys(oData[k])) { if (p !== "locales") { str += "<li>"+ s16 + p +": "+ sc + oData[k][p] +"</li>" } } displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } } if (isLoop) { let testCount = localeGroups.length // record it if (undefined == oTestData[testCount]) {oTestData[testCount] = []} oTestData[testCount].push(numsys) dom.results.innerHTML = dom.results.innerHTML + '<br>' + '\''+ numsys +' ' + testCount return } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // display let display = s4 + localeGroups.length + sc +" from "+ s4 + aLocales.length + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } Promise.all([ get_globals() ]).then(function(){ buildnav() try { // pointless if we can't use the feature being tested: FF58+ let test = new Intl.NumberFormat("en").formatToParts(1.1) isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // check bigint support: FF68+ try { let y = BigInt("9999999999999999") isBigIntSupported = true } catch(e) {} // add additional locales to core locales for this test let aListExtra = [ "ar-dj,arabic (djibouti)", "ar-dz,arabic (algeria)", "ff-adlm,fulah (adlam)", "it-ch,italian (switzerland)", 'kk-cn,kazakh (china)', "ru-ua,russian (ukraine)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "yue-cn,cantonese (china)", // blink 'pa-pk,punjabi (pakistan)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en'] legend() if (isSupported) { setTimeout(function() { run_main() }, 100) } }) </script> </body> </html> ================================================ FILE: tests/nfnotation.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>nf: notation</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 780px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">numberformat: notation <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy: excluding compact, currency and unit as they are covered elsewhere</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], isSupported = false, isBigIntSupported = false, localesHashAll = "" // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.NumberFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" let tests = {} let nBig if (isBigIntSupported) { nBig = BigInt("987354000000000000") } if (method == "min") { tests = { "scientific": { "decimal": [987654], // swap with bigint }, "standard": { "decimal": [0/0, -1000, 987654], "percent": [1000], }, } if (isBigIntSupported) { tests.scientific.decimal = [nBig] // replace: use at least 1 bigint (either works for entropy) } } else { // standard (default), scientific, engineering // style: decimal (default), currency, percent, unit tests = { "engineering": { "decimal": [-1, 0, 0/0, 1/10, 1/1000, 987654], }, "scientific": { "decimal": [-1, 0, 0/0, 1/10, 1/1000, 987654], }, "standard": { "decimal": [-1, 0, 0/0, 1/10, -1000, 987654], "percent": [1/100, 1000, 987654321, Infinity], }, } if (isBigIntSupported) { tests.engineering.decimal.push(nBig) tests.scientific.decimal.push(nBig) tests.standard.decimal.push(nBig) } } let oStyles = {} try { aLocales.forEach(function(code) { // for each locale oStyles = {} Object.keys(tests).sort().forEach(function(not){ // for each notation oStyles[not] = {} Object.keys(tests[not]).forEach(function(s){ // for each style oStyles[not][s] = [] try { let formatter = Intl.NumberFormat(code, {notation: not, style: s}) tests[not][s].forEach(function(n){ // for each number oStyles[not][s].push(formatter.format(n)) }) } catch (e) {console.log(e.name, e.message)} // ignore invalid }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oStyles).forEach(function(not){ // each notation oTempData[hash][not] = {} Object.keys(oStyles[not]).forEach(function(s){ // for each style if (oStyles[not][s].length) { oTempData[hash][not][s] = oStyles[not][s].join(" | ") } }) }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const not of Object.keys(oTempData[h]).sort()) { // for each notation if (not == "locales") { oData[h][not] = oTempData[h][not].join(", ") } else { if (Object.keys(oTempData[h][not]).length) { oData[h][not] = oTempData[h][not] } } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { // for each hash localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length let str = "" for (const not of Object.keys(oData[k])) { // for each notation if (not !== "locales") { str += "<li>"+ s12 + not + sc +"</li>" Object.keys(oData[k][not]).forEach(function(s){ // for each style str += s16 + s +": "+ sc + oData[k][not][s] +"</br>" }) } } // wrap into details for long lists if (Object.keys(tests).length > 15) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "a5e43330") { // FF151+ } else if (resultsHash == "c3ccc494") { // FF147-150 } else if (resultsHash == "cf242641") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "247165ee") { // FF151+: 83 } else if (localesHash == "37083b3a") { // FF147-150: 83 } else if (localesHash == "6fe4f315") { // FF140-146: 80 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat // standard (default), scientific, engineering // style: decimal (default), currency, percent, unit try { // pointless if we can't use the feature being tested: FF78+ let test = new Intl.NumberFormat("en-US", {notation: "scientific"}).format(987654321) if (test.length == 7) { isSupported = true } else { dom.results.innerHTML = s4 + "notation:" + sc +" not supported" } } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // check bigint support: FF68+ try { let y = BigInt("9999999999999999") isBigIntSupported = true } catch(e) {} // add additional locales to core locales for this test let aListExtra = [ "ar-dj,arabic (djibouti)", "ar-dz,arabic (algeria)", "en-au,english (australia)", "en-se,english (sweden)", "ff-adlm,fulah (adlam)", "it-ch,italian (switzerland)", 'kk-cn,kazakh (china)', "ru-ua,russian (ukraine)", "ur-in,urdu (india)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "yue-hans,cantonese (simplified)", // blink "fr-ch,french (switzerland)", 'pa-arab,punjabi (arabic)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/nfsign.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>nf: signdisplay</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">numberformat: signdisplay <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], isSupported = false, isBigIntSupported = false, localesHashAll = "" // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.NumberFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat let items = [-1, -0, 0/0, 0, 1] let tests = { "always": items, "auto": items, "exceptZero": items, //"negative":items, // RangeError: invalid value "negative" for option signDisplay "never": items, } if (method == "min") { tests = { "always": [-1, 0/0] } } try { aLocales.forEach(function(code) { let oStyles = {} Object.keys(tests).forEach(function(s){ oStyles[s] = [] let formatter = new Intl.NumberFormat(code, {signDisplay: s}) tests[s].forEach(function(t){ oStyles[s].push(formatter.format(t)) }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(tests).forEach(function(s){ oTempData[hash][s] = oStyles[s].join(" | ") }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == "locales") { oData[n][p] = oTempData[n][p].join(", ") } else { oData[n][p] = oTempData[n][p] } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let localeCount = oData[k]["locales"].split(",").length let str = "" for (const p of Object.keys(oData[k])) { if (p !== "locales") { str += "<li>"+ s16 + p +": "+ sc + oData[k][p] +"</li>" } } displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "4491496a") { // FF147+ } else if (resultsHash == "2a5a0fce") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "492461bc") { // FF147+: 43 } else if (localesHash == "68ebd8fb") { // FF140-146: 40 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat // note: numberformat supported since at least FF29 // FF78+ : signDisplay // FF93+ : negative try { // pointless if we can't use the feature being tested: FF78+ let test = new Intl.NumberFormat("en", {signDisplay: "always"}).format(5) test += "" if (test.length > 1) { isSupported = true } else { dom.results.innerHTML = s4 + "signDisplay:" + sc +" not supported" } } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // check bigint support: FF68+ try { let y = BigInt("9999999999999999") isBigIntSupported = true } catch(e) {} // add additional locales to core locales for this test let aListExtra = [ "ar-er,arabic (eritrea)", "ff-adlm,fulah (adlam)", 'kk-cn,kazakh (china)', "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "yue-hans,cantonese (simplified)", // blink 'pa-pk,punjabi (pakistan)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() if (isSupported) { setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/nfunit.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>nf: unitdisplay</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">numberformat: unitdisplay <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btnfirst" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> &nbsp; KEY: <span class="s16">L</span> long <span class="s16">N</span> narrow <span class="s16">S</span> short <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, oUnits = {}, aLegend = [], aLocales = [], oTestData = {}, isSupported = false, isBigIntSupported = false, localesHashAll = "", // to compare min to strWarning = " don't panic, it's working<br><br> ... running 'ALL' takes a few seconds" function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function set_units() { if (!isSupported) {return} // https://github.com/unicode-org/cldr/blob/main/common/validity/unit.xml\ // https://unicode.org/reports/tr35/tr35-general.html#63-example-units // all let units = [ // https://searchfox.org/mozilla-central/source/js/src/builtin/intl/SanctionedSimpleUnitIdentifiersGenerated.js // FF: simple units "acre","bit","byte","celsius","centimeter", "day","degree","fahrenheit","fluid-ounce","foot", "gallon","gigabit","gigabyte","gram","hectare", "hour","inch","kilobit","kilobyte","kilogram", "kilometer","liter","megabit","megabyte","meter", "microsecond","mile","mile-scandinavian","milliliter","millimeter", "millisecond","minute","month","nanosecond","ounce", "percent","petabyte","pound","second","stone", "terabit","terabyte","week","yard","year", // combined // we already get the simple units in single/plural // so we only need one of these to get "per" (in case it holds entropy) // not true for all "liter-per-kilometer", "kilometer-per-hour", // this covers it "meter-per-second", "mile-per-gallon", "mile-per-hour", //* other // acceleration "g-force","meter-per-square-second", // angle "revolution","radian","arc-minute","arc-second", // area "square-kilometer","square-inch","dunam", // concentration "karat","milligram-per-deciliter","millimole-per-liter","permillion", "permille","permyriad","mole","liter-per-100-kilometer", "mile-per-gallon-imperial","petabyte", // duration "century","day-person","month-person","week-person","year-person", // electric "ampere","milliampere","ohm","volt", // energy "kilocalorie","calorie","foodcalorie","kilojoule","joule","kilowatt-hour","electronvolt", "british-thermal-unit", // force "pound-force","newton", // frequency "gigahertz","megahertz","kilohertz","hertz", // length "parsec","light-year","astronomical-unit","furlong","fathom", "nautical-mile","point","solar-radius","lux","solar-luminosity", // mass "metric-ton","ounce-troy","carat","dalton","earth-mass","solar-mass", // power "gigawatt","milliwatt","horsepower", // pressure "hectopascal","millimeter-ofhg","pound-force-per-square-inch","inch-ofhg", "millibar","atmosphere","kilopascal","megapascal", // speed "knot", // temperature "generic","kelvin", // torque "pound-force-foot","newton-meter", // volume "cubic-kilometer","cubic-inch","megaliter","pint","cup", "fluid-ounce-imperial","tablespoon","teaspoon","barrel", //*/ ] units.sort() units = units.filter(function(item, position) {return units.indexOf(item) === position}) oUnits["all"] = units // supported: ignore invalid unit identifiers let supported = [] units.forEach(function(u) { try { let formatter = Intl.NumberFormat("en", {style: "unit", unit: u}) supported.push(u) } catch(e) {} }) oUnits["supported"] = supported } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.NumberFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" oTestData = {} let tests = {} let units = oUnits["supported"] let nBig if (isBigIntSupported) { nBig = BigInt("987354000000000000") } if (method == "min") { let optN = {"narrow": [1]}, optL = {"long": [1]}, both = {"long": [1], "narrow": [1],}, everything = {"long": [1], "narrow": [1], "short": [987654]} tests = { // FF115+ "fahrenheit": both, "foot": optL, "hectare": {"long": [1], "short": [987654]}, "kilometer-per-hour": optN, "millimeter": optN, "month": both, "nanosecond": optN, "percent": everything, "second": {"long": [1], "narrow": [1], "short": [987654]}, "terabyte": optL, // FF121+ ICU 74 "byte": optN, } // FF109 and lower needs some help: swap month long for short if (isFF && "object" !== typeof ondeviceorientationabsolute) { tests["month"] = {"narrow": [1], "short": [987654]} tests["day"] = optL tests["gallon"] = {"short": [987654]} } if (!isFF) { // chrome tests["gallon"] = {"short": [987654]} // non-gecko: make month = all three to cover bases tests["month"] = {"long": [1], "narrow": [1], "short": [987654]} } } else { // unitdisplay let unitDisplays = ["long","narrow","short"] // 184 /* // short adds 1 region (splits es-bo + es-py) unitDisplays = ["long"] // 169 unitDisplays = ["narrow"] // 176 unitDisplays = ["short"] // 172 unitDisplays = ["long","narrow"] // 183 unitDisplays = ["long","short"] // 179 unitDisplays = ["narrow","short"] // 180 //*/ units.forEach(function(u) { try { let formatter = Intl.NumberFormat("en", {style: "unit", unit: u}) // ignore invalid unit identifiers tests[u] = {} unitDisplays.forEach(function(ud) { // we only need one test from each but we need both values if (ud == "short") { tests[u][ud] = [987654] } else { tests[u][ud] = [1] } // bigint doesn't add anything /* if (isBigIntSupported) { tests[u][ud].push(nBig) } */ }) } catch(e) {} }) } function get_value(type, parts) { try { let str = "none" for (let i = 0 ; i < parts.length; i++) { if (parts[i]["type"] === type) { str = parts[i]["value"] str += str.length == 1 ? " ("+ str.charCodeAt(0) +")" : "" str = str.charCodeAt(0) return str // no need to keep checking } } return str } catch(e) { return "error" } } let oStyles = {} try { aLocales.forEach(function(code) { // for each locale oStyles = {} Object.keys(tests).sort().forEach(function(u){ // for each unit oStyles[u] = {} Object.keys(tests[u]).sort().forEach(function(ud){ // for each unitdisplay oStyles[u][ud] = [] let isFound = false try { let formatter = Intl.NumberFormat(code, {style: "unit", unit: u, unitDisplay: ud}) tests[u][ud].forEach(function(n){ // for each number oStyles[u][ud].push(formatter.format(n)) /* temp: checking for a test that returns a literal most/all of the time let testvalue = get_value('literal', formatter.formatToParts(n)) if ('number' == typeof testvalue) { if (undefined == oTestData[u+'_'+ud]) {oTestData[u+'_'+ud] = {}} if (undefined == oTestData[u+'_'+ud][testvalue]) { oTestData[u+'_'+ud][testvalue] = [code]; isFound = true } else if (!isFound) { // don't duplicate oTestData[u+'_'+ud][testvalue].push(code); isFound = true } } //*/ }) } catch (e) {} // ignore invalid }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] Object.keys(oStyles).forEach(function(u){ // each unit oTempData[hash][u] = {} Object.keys(oStyles[u]).forEach(function(ud){ // for each unitdisplay if (oStyles[u][ud].length) { oTempData[hash][u][ud] = oStyles[u][ud].join(" | ") } }) }) } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order in new object for (const h of Object.keys(oTempData).sort()) { // for each hash oData[h] = {} for (const u of Object.keys(oTempData[h]).sort()) { // for each unit if (u == "locales") { oData[h][u] = oTempData[h][u].join(", ") } else { if (Object.keys(oTempData[h][u]).length) { oData[h][u] = oTempData[h][u] } } } } let localeGroups = [], displaylist = [] for (const h of Object.keys(oData)) { // for each hash localeGroups.push(oData[h]["locales"]) let localeCount = oData[h]["locales"].split(",").length let str = "" for (const u of Object.keys(oData[h])) { // for each unit if (u !== "locales") { str += "<li>"+ s12 + u +": "+ sc let items = [] Object.keys(oData[h][u]).forEach(function(ud){ // for each unitdisplay let abbrev = ud.slice(0,1).toUpperCase() items.push(s16 + abbrev +": "+ sc + oData[h][u][ud]) }) str += items.join(" ") } str += "</li>" } // wrap into details for long lists if (Object.keys(tests).length > 15) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + h + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[h]["locales"] +"</li></ul>" ) } // hashes + btns let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) sDetail["results"] = oData localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "5549ad9b") { // FF151+ } else if (resultsHash == "46a2bf50") { // FF147-150 } else if (resultsHash == "f6967327") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "e4adc310") { // FF147+ 209 } else if (localesHash == "85300889") { // FF140-146 206 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" let status = "calculating ..." if (method == "all") {status += strWarning} dom.results.innerHTML = status // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat // FF78+ : unit, unitDisplay try { // pointless if we can't use the feature being tested let test = new Intl.NumberFormat(undefined, {style: "unit", unit: "mile-per-hour", unitDisplay: "long"}).format(5) isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // check bigint support: FF68+ try { let y = BigInt("9999999999999999") isBigIntSupported = true } catch(e) {} // add additional locales to core locales for this test let aListExtra = [ "ar-bh,arabic (bahrain)", "ar-dz,arabic (algeria)", "ar-sa,arabic (saudi arabia)", "bs-cyrl,bosnian (cyrillic)", "de-at,german (austria)", "de-ch,german (switzerland)", "de-li,german (liechtenstein)", "en-ag,english (antigua & barbuda)", "en-at,english (austria)", "en-au,english (australia)", "en-be,english (belgium)", "en-bs,english (bahamas)", "en-ca,english (canada)", "en-ch,english (switzerland)", "en-fi,english (finland)", "en-in,english (india)", "en-za,english (south africa)", "es-ar,spanish (argentina)", "es-bo,spanish (bolivia)", "es-br,spanish (brazil)", "es-co,spanish (colombia)", "es-cr,spanish (costa rica)", "es-do,spanish (dominican republic)", "es-mx,spanish (mexico)", "es-pr,spanish (puerto rico)", "es-py,spanish (paraguay)", "es-us,spanish (united states)", "ff-adlm,fulah (adlam)", "fr-ca,french (canada)", "fr-ch,french (switzerland)", "fr-ht,french (haiti)", "fr-lu,french (luxembourg)", "hi-latn,hindi (latin)", "it-ch,italian (switzerland)", 'kk-cn,kazakh (china)', "kok-latn,konkani (latin)", "ms-bn,malay (brunei)", "ps-pk,pashto (pakistan)", "pt-ao,portuguese (angola)", "qu-bo,quechua (bolivia)", "ro-md,romanian (moldova)", "sr-cyrl-ba,serbian (cyrillic bosnia & herzegovina)", "sr-cyrl-me,serbian (cyrillic montenegro)", "sr-latn-ba,serbian (latin bosnia & herzegovina)", "sv-fi,swedish (finland)", "sw-cd,swahili (congo kinshasa)", "sw-ke,swahili (kenya)", "ta-my,tamil (malaysia)", "ur-in,urdu (india)", "uz-cyrl-uz,uzbek (cyrillic uzbekistan)", "yo-bj,yoruba (benin)", "yue-hans,cantonese (simplified)", // blink 'pa-pk,punjabi (pakistan)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() if (isSupported) { set_units() setBtn("all") dom.results.innerHTML = "calculating ..."+ strWarning setTimeout(function() { run_main("all") }, 50) } }) </script> </body> </html> ================================================ FILE: tests/os.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>os</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <!-- custom --> <style> table {width: 480px;} .oscenter { text-align: center; } .cssDocFont {font-family: "Arial Black";} </style> </head> <body> <div class="offscreen"> <div class="cssDocFont" id="divDocFont"></div> </div> <div class="hidden"> <input type="radio" id="wgtradio"> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#feature">return to TZP index</a></td></tr> </table> <table id="tb3"> <thead><tr><th> <div class="nav-title">os <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td class="intro"> <span class="no_color">gecko (FF89+) only: testing os detection logic</span> </td></tr> <tr><td><hr><br></td></tr> <tr><td style="text-align: left;"> <div>OS &nbsp; <p class="c mono spaces no_color" id="os"> &nbsp; </p><br> </div> <div>DEBUG <p class="c mono spaces no_color" id="debug"> &nbsp; </p> </div> </td></tr> </table> <br> <script> 'use strict'; let aDebug = [], oResults = {}, t0, pad = 25, isTimeOut = false, isThrowError = false, isThrowWidgetError = false, isThrowDocFontError = false, isThrowFontError = false, isOnlyGecko = false let notNormal = sb +" not normal"+ sc +" [now you <b><u>really</u></b> stand out]", goodResult = sg +"result: "+ sc, badResult = sb +"result: "+ sc let oMap = { 1: goodResult +"you are not gecko", 2: goodResult +"update your browser" +"<br><br><div class='indent faint'>this PoC requires FF89+ to be fully effective</div>", "01": "desktop", "02": "chrome", "03": "widget", "04": "fonts", "05": "currentTime", } function finish(type) { dom.perf = Math.round((performance.now() - t0)) + " ms" dom.debug.innerHTML = aDebug.join("<br>") if (!isFF) (type = 1) if (type !== undefined && type < 3) { dom.os.innerHTML = oMap[type] return } else { let display = [] for (const k of Object.keys(oResults)) { let name = oMap[k] display.push(s13 + (name+": ").padStart(15) +sc + oResults[k]) } dom.os.innerHTML = display.join("<br>") } } const check_css = (isNew) => new Promise(resolve => { setTimeout(() => resolve("timed out"), 100) // FF89-123 // 1280128: FF51+ win/mac | 1701257: FF89+ linux, therefore undefined = android // FF121+: 1855861 // FF124+: 1874232 // new: chrome://browser/content/extension-popup-panel.css // fallback: chrome://browser/content/extension.css // both these are desktop only let newCounter = 0, tmpOS function exit() { return resolve((isNew ? newCounter : tmpOS)) } const get_event = (css, item) => new Promise(resolve => { css.onload = function() { if (isNew) { //desktop vs android newCounter++ } else { tmpOS = item == "win" ? "windows" : item } count++ document.head.removeChild(css) aDebug.push(s3 + (item +": ").padStart(pad) + sc +"detected") if (count == maxCount) {exit()} return resolve() } css.onerror = function() { count++ document.head.removeChild(css) aDebug.push(s3 + (item +": ").padStart(pad) + sc +"not detected") if (count == maxCount) {exit()} return resolve() } }) let count = 0, maxCount let path = "chrome://browser/content/extension-", suffix = "-panel.css" let list = ["win", "mac","linux"] if (isNew) { list = ['extension'] if (isFF && isVer > 123) { list = ['extension-popup-panel','extension'] } path = "chrome://browser/content/", suffix = ".css" if (isFF) { let string = (isVer > 123 ? "124+ " : "89+ ") + "desktop: " aDebug.push(s13 + string.padStart(pad) + sc + path +"*.css") } else { aDebug.push(s13 + "desktop: ".padStart(pad) + sc + path +"*.css") } } else { aDebug.push(s13 + "89-123 chrome://: ".padStart(pad) + sc +"browser/content/extension-*"+ suffix) } if (!isTimeOut) { try { if (isThrowError) {hortonhearsawho++} maxCount = list.length list.forEach(function(item) { let css = document.createElement("link") css.type = "text/css" css.rel = "stylesheet" css.href = path + item + suffix document.head.appendChild(css) get_event(css, item) }) } catch(e) { aDebug.push(sb + ("error: ").padStart(pad) + sc) aDebug.push("<div class='oscenter faint'>"+ e +"</div>") return resolve("error") } } }) function check_docfonts() { // FF124+ desktop: dig deeper // the easiest way is to check for '-apple-system', 'MS Shell Dlg','MS Shell Dlg \\32' // we could check a widget fonts but that fails in TB windows, may change // and getComputedStyle can report the wrong font, so detect the actual fonts let key = "04" // this is for fonts test if we fail try { if (isThrowDocFontError) {i_am_groot++} // test doc fonts enabled let fntTest = "\"Arial Black\"" let font = getComputedStyle(dom.divDocFont).getPropertyValue("font-family") let fntEnabled = (font == fntTest ? true : false) if (!fntEnabled) { if (font.slice(0,11) == "Arial Black") {fntEnabled = true} // ext may strip quotes marks } if (fntEnabled) { aDebug.push(s13 + "document fonts: ".padStart(pad) + sc +"enabled") try_fonts() } else { aDebug.push(s13 + "document fonts: ".padStart(pad) + sc +"disabled") aDebug.push(s13 + "fonts: ".padStart(pad) + sc + zNA +"<br>") oResults[key] = zNA try_somethingelse() } } catch(e) { aDebug.push(s13 + "document fonts: ".padStart(pad) + sc) aDebug.push(s13 + "fonts: ".padStart(pad) + sc + zNA) aDebug.push(sb + "error: ".padStart(pad) + sc) aDebug.push("<div class='oscenter faint'>"+ e +"</div>") oResults[key] = zNA try_somethingelse() } } function check_widgetfonts() { let key = "03" try { if (isThrowWidgetError) {green_eggs_and_ham++} let aIgnore = [ 'cursive','emoji','fangsong','fantasy','math','monospace','none','sans-serif', 'serif','system-ui','ui-monospace','ui-rounded','ui-serif','undefined', undefined, ''] let font = getComputedStyle(dom.wgtradio).getPropertyValue("font-family") //font = "Roboto" //font = "" // godamnit pierov let fontDisplay = font == "" ? "empty string" : font aDebug.push(s13 + "widget font: ".padStart(pad) + sc + fontDisplay) let display, value if (isFF) { if (aIgnore.includes(font)) { value = zNA display = sb + ("result: ").padStart(pad) + sc + "too generic" } else { let systemfont if (font.slice(0,12) == "MS Shell Dlg") {systemfont = "windows" } else if (font == "-apple-system") {systemfont = "mac"} if (systemfont !== undefined) { value = systemfont display = sg + ("result: ").padStart(pad) + sc + value } else { value = font display = s13 + ("result: ").padStart(pad) + sc + "maybe useful" } } } else { display = s3 + ("result: ").padStart(pad) + sc + zNA } if (display !== undefined) {aDebug.push(display +"<br>")} if (isFF) {oResults[key] = value} check_docfonts() } catch(e) { aDebug.push(s13 + "widget font: ".padStart(pad) + sc) aDebug.push(sb + "error: ".padStart(pad) + sc) aDebug.push("<div class='oscenter faint'>"+ e +"</div>") oResults[key] = zNA check_docfonts() } } function run() { t0 = performance.now() dom.os.innerHTML = "" dom.debug.innerHTML = "" aDebug = [] oResults = {} let notation = "", isVerOpen, isVerFull let resFF = isOnlyGecko ? isFF : (isFF ? true : zNA) aDebug.push(s13 + "gecko: ".padStart(pad) + sc + resFF) if (isFF) { isVerOpen = (isVer == isVerMax +"") isVerFull = isVer + (isVerOpen ? "+" : "") aDebug.push(s13 + "version: ".padStart(pad) + sc + isVer + (isVerOpen ? "+" : (isVer == 52 ? " or lower" : "")) +"<br>") } else { aDebug.push(s13 + "version: ".padStart(pad) + sc + zNA +"<br>") } function analyze(isNew, result) { let key = isNew ? "01" : "02" let value = result, display if (result == "timed out") { display = sb + ("timed out: ").padStart(pad) + sc value = zNA } else if (result == "error") { value = zNA } else { if (isFF && isNew) { // 6 results: 0,1,2 for > 123 and < 124 if (result == 0) { value = "android" display = sg + "result: ".padStart(pad) + sc + value } else if (isVer > 123 && result == 1) { value = "desktop" display = sg + "result: ".padStart(pad) + sc + value + sb +" [but only 1 of 2]"+ sc } else { value = "desktop" display = sg + "result: ".padStart(pad) + sc + value } } else if (isFF && !isNew) { if (result == undefined) { if (isVer > 123) { value = zNA display = s13 + "result: ".padStart(pad) + sc + value } else { value = "android" // FF123 or lower nothing found: assume android display = sg + "result: ".padStart(pad) + sc + value } } else { // if not undefined, we can only have windows/mac/linux display = sg + "result: ".padStart(pad) + sc + result } } else { display = s13 + "result: ".padStart(pad) + sc + zNA } } if (display !== undefined) {aDebug.push(display +"<br>")} if (isFF) {oResults[key] = value} } if (isOnlyGecko && !isFF) { finish(1) } else if (isOnlyGecko && !isFF || isFF && isVer < 89) { finish(2) } else { Promise.all([ check_css(true) ]).then(function(res){ analyze(true, res[0]) Promise.all([ check_css(false) ]).then(function(res){ analyze(false, res[0]) check_widgetfonts() }) }) } } function try_somethingelse() { // is there anything that is platform specific that can't be flipped with a pref // FF34+ 848954 // linux doesn't have a currentTime property in HTMLMediaElement // https://searchfox.org/mozilla-central/source/dom/html/HTMLMediaElement.h#37 // hmm ok , not what I thought it was /* let key = "05" aDebug.push(s13 + "HTMLMediaElement: ".padStart(pad) + sc + "currentTime") try { let value = HTMLMediaElement.prototype.hasOwnProperty("currentTime") ? "not linux" : "linux" // 0.008ms aDebug.push(sg + "result: ".padStart(pad) + sc + value +"<br>") oResults[key] = value } catch(e) { aDebug.push(sb + "error: ".padStart(pad) + sc) aDebug.push("<div class='oscenter faint'>"+ e +"</div>") oResults[key] = zNA } */ aDebug.push(s13 + "something else: ".padStart(pad) + sc +"pending") finish() } setTimeout(function() { Promise.all([ get_globals() ]).then(function(){ Promise.all([ get_isVer() ]).then(function(){ run() }) }) }, 25) function try_fonts() { let key = "04" let fntFake = "--00"+ rnd_string() let fntSize = "512px" let fntString = "Mōá?-"+ String.fromCodePoint('0xFFFF') let fntTest = [ 'Dancing Script','Roboto', '-apple-system', 'MS Shell Dlg \\32', ] fntTest.push(fntFake) fntTest.sort() let fntControl = ['monospace, Consolas, Menlo, "Courier New\"','sans-serif, Arial','serif, "Times New Roman\"'] let fntGeneric = fntControl try { if (isThrowFontError) {let_them_eat_cake++} const doc = document // or iframe.contentWindow.document const id = `font-fingerprint` const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) set_element(id, fntSize, fntString) const span = doc.getElementById(`${id}-detector`) const originPixelsToNumber = pixels => 2*pixels.replace('px', '') const detectedFonts = new Set() const style = getComputedStyle(span) let getDimensions = (span, style) => { const transform = style.transformOrigin.split(' ') const dimensions = { height: originPixelsToNumber(transform[1]), width: originPixelsToNumber(transform[0]), } return dimensions } // base sizes let base = fntGeneric.reduce((acc, font) => { span.style.font = "" span.style.setProperty('--font', font) const dimensions = getDimensions(span, style) acc[font.split(",")[0]] = dimensions // use only first name, i.e w/o fallback return acc }, {}) span.style.font = "" // reset // test validity let baseStyle = "monospace" let wValue = base[baseStyle].width, hValue = base[baseStyle].height let wType = typeFn(wValue) let hType = typeFn(hValue) if ("number" !== wType || "number" !== hType) { try {document.getElementById("font-fingerprint").remove()} catch(e) {} aDebug.push(s13 + "fonts: ".padStart(pad) + sc) aDebug.push(sb + "type error: ".padStart(pad) + sc + wType +" x "+ hType +"<br>") oResults[key] = zNA try_somethingelse() return } let isDetected = false fntTest.forEach(font => { isDetected = false // have we found it fntControl.forEach(basefont => { if (isDetected) {return} const family = "'"+ font +"', "+ basefont span.style.setProperty('--font', family) const style = getComputedStyle(span) const dimensions = getDimensions(span, style) basefont = basefont.split(",")[0] // switch to short generic name if (dimensions.width != base[basefont].width || dimensions.height != base[basefont].height) { detectedFonts.add(font) isDetected = true // we're only testing one method } return }) }) let aFonts = [...detectedFonts] // testing //aFonts = ['Dancing Script','Roboto','-apple-system','MS Shell Dlg \\32'] //aFonts.push(fntFake) //aFonts = ['MS Shell Dlg \\32'] //aFonts = ['-apple-system'] //aFonts = ['-apple-system','MS Shell Dlg \\32'] //aFonts = ['Dancing Script','Roboto'] //aFonts = ['Roboto'] //aFonts = ['Dancing Script'] //aFonts = [] aFonts.sort() let len = aFonts.length, display, value if (len == 1) { let fnt0 = aFonts[0] aDebug.push(s13 + "fonts: ".padStart(pad) + sc + fnt0) if (fnt0 == "MS Shell Dlg \\32") { value = "windows" } else if (fnt0 == "-apple-system") {value = "mac"} if (value !== undefined) { display = sg + "result: ".padStart(pad) + sc + value } } else { if (len == 2) { aDebug.push(s13 + "fonts: ".padStart(pad) + sc + aFonts.join(", ")) } else { aDebug.push(s13 + "fonts: ".padStart(pad) + sc + (aFonts.length ? len : "none") + " detected") aFonts.forEach(function(f) { aDebug.push("".padStart(pad) + f) }) } } if (aFonts.join() == "Dancing Script,Roboto" || aFonts.join() == "Dancing Script" || aFonts.join() == "Roboto") { if (oResults["01"] == "android" && oResults["03"] == "Roboto") { value = "android" display = sg + "result: ".padStart(pad) + sc + value + sg +" [see desktop + widget test]"+ sc } else if (oResults["01"] !== "desktop" && oResults["03"] == "Roboto" && aFonts.includes('Dancing Script')) { value = "android" display = sg + "result: ".padStart(pad) + sc + value + sg +" [Dancing Script + widget test]"+ sc } else { value = aFonts.join() display = s13 + "result: ".padStart(pad) + sc + "maybe android"+ sc } } if (len == 0) { if (oResults["01"] == "desktop" && oResults["03"] !== "windows" && oResults["03"] !== "mac") { value = "linux" display = sg + "result: ".padStart(pad) + sc + "linux"+ sc +" [see desktop + widget test]"+ sc } } if (aFonts.includes(fntFake)) { // also covers all fonts returned value = zNA display = sb + "result: ".padStart(pad) + sc + "fake font detected" } else if ( aFonts.includes("-apple-system") && aFonts.includes('MS Shell Dlg \\32') ) { value = zNA display = sb + "result: ".padStart(pad) + sc + "windows and mac detected" } if (display == undefined) { value = (len == 0 ? "none" : aFonts.join(", ")) display = s13 + "result: ".padStart(pad) + sc + "pending"+ sc } if (display !== undefined) { aDebug.push(display +"<br>") oResults[key] = value try_somethingelse() return } } catch(e) { try {document.getElementById("font-fingerprint").remove()} catch(e) {} aDebug.push(s13 + "fonts: ".padStart(pad) + sc) aDebug.push(sb + "error: ".padStart(pad) + sc) aDebug.push("<div class='oscenter faint'>"+ e +"</div>") oResults[key] = zNA try_somethingelse() } } function set_element(id, fntSize, fntString) { document.getElementById(id).innerHTML = ` <style> #${id}-detector { --font: ''; position: absolute !important; left: -9999px!important; font-size: ` + fntSize + ` !important; font-style: normal !important; font-weight: normal !important; letter-spacing: normal !important; line-break: auto !important; line-height: normal !important; text-transform: none !important; text-align: left !important; text-decoration: none !important; text-shadow: none !important; white-space: normal !important; word-break: normal !important; word-spacing: normal !important; /* in order to test scrollWidth, clientWidth, etc. */ padding: 0 !important; margin: 0 !important; /* in order to test inlineSize and blockSize */ writing-mode: horizontal-tb !important; /* for transform and perspective */ transform-origin: unset !important; perspective-origin: unset !important; } #${id}-detector::after { font-family: var(--font); content: '` + fntString + `'; } </style> <span id="${id}-detector"></span>` } </script> </body> </html> ================================================ FILE: tests/pointerevent.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>pointer & touch events</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <!-- custom --> <style> table {width: 580px;} .reset { float: left; display: flex; align-items: center; justify-content: center; height: 50px; width: 110px; color: var(--test0); border: 2px solid var(--test7); cursor: pointer; margin-bottom: 20px; } .pointer { float: left; display: flex; align-items: center; justify-content: center; height: 210px; width: 110px; color: var(--test0); border: 2px solid var(--test7); cursor: pointer; margin-bottom: 20px; } .flex-item { text-align: center; margin: 10px; } </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#devices">return to TZP index</a></td></tr> </table> <table id="tb7"> <col width="23%"><col width="77%"> <thead><tr><th colspan="2"> <div class="nav-title">pointer & touch events</div> </th></tr></thead> <tr><td colspan="2"></td></tr> <tr> <td> <div class="reset" id="reset"><div class="flex=item" id="resettext">1: START</div></div> <div class="pointer" id="target"> <div class="flex-item"><br>2: CLICK<br><br>context click<br>(if you can)<br><br>otherwise<br><br>keep stabbing<br>and moving</div> </div> <div class="reset"><div class="flex=item">3: FINISH</div></div> </td> <td class="mono" style="text-align: left; vertical-align: top;"> <div class="s6">OVERALL HASH <span class="no_color" id="hash"></span></div><br></div> <div class="s4"><u>POINTER</u> <span class='no_color spaces' id='pointerevents'></span> <span class="no_color" id="pointerhash"></span> </div><br> <div class="no_color spaces" id="results"></div> <div class="s4"><u>TOUCH</u> <span class='no_color spaces' id='touchevents'></span> <span class="no_color" id="touchhash"></span> </div><br> <div class="no_color spaces" id="touch"></div><hr><br> <div class="s6 spaces"> maxTouchPoints <span class="no_color" id="maxTouchPoints"></span></div></div><br> <div class="s6">touch properties <span class="no_color spaces" id="touch_properties"></span></div><br> <div class="s4"><u>EVENTS</u></div><br><div class="no_color" id="events"></div><br> <div class="s4"><u>POINTERRAWUPDATE</u> <span class="no_color spaces" id='raw'></span></div> </td> </tr> </table> <br> <script> 'use strict'; // https://bugzilla.mozilla.org/show_bug.cgi?id=1363508 let padlen = 18 let oTemp = {'pointer':{},'touch':{}} let oData = {} // sorted oTemp for display and oFP let oFP = { 'maxTouchPoints': '', 'pointer': {}, 'pointerrawupdate': '', 'touch': {}, 'touch_properties': '', } let isStart = true, hasKeys = false let setEvents = new Set() let setRaw = new Set() // sorted list: these are all numbers let aList = [ 'clientX','clientY', 'force', // float 0.0 (no pressure) - 1.0 (max pressure) 'radiusX','radiusY', 'rotationAngle', 'screenX', 'screenY', // RFP = matches clientX/Y ] // not sorted: so we group display items let oList = { pointerId: "number", // isPrimary: "boolean", // RFP true pressure: "number", // RFP: 0 if not active, 0.5 if active mozPressure: "number", // RFP should always return 0.5 now pointerType: "string", // RFP mouse mozInputSource: "number", // RFP should be 1 now tangentialPressure: "number", // RFP 0 tiltX: "number", // RFP 0 tiltY: "number", // RFP 0 twist: "number", // RFP 0 width: "number", // RFP 1 height: "number", // RFP 1 altitudeAngle: "number", azimuthAngle: "number", } // events let oDataEvents = { 'pointer': ['down','enter','leave','move','over','out','up'], 'touch': ['cancel','end','move','start'], } let oNonDataEvents = { 'mouse': ['down','enter','leave','move','out','over','up'], 'pointer': ['cancel'], //'touch': ['cancel'], } function finish() { // check all events have been recorded // pointer let expected = oDataEvents.pointer.length let count = Object.keys(oTemp.pointer).length if (count !== expected) {return} // touch expected = oDataEvents.touch.length count = Object.keys(oTemp.touch).length if (hasKeys && count !== expected) {return} // add pointerrawupdate let aRaw = Array.from(setRaw) oFP.pointerrawupdate = aRaw.join(', ') // sort & filter object for consistent hashes for (const k of Object.keys(oTemp).sort()) { oData[k] = {} for (const n of Object.keys(oTemp[k]).sort()) {oData[k][n] = oTemp[k][n]} } // grab hashes let hash = mini(oFP.pointer) dom.pointerhash.innerHTML = hash hash = mini(oFP.touch) if (0 == count) {hash = 'none'; oFP.touch = 'none'} dom.touchhash.innerHTML = hash hash = mini(oFP) dom.hash.innerHTML = hash + '<span class="spaces"><br><br>' + json_stringify(oFP) +"</span>" console.log('fingerprint\n', oFP) console.log('data\n', oData) } function runtouch(event, type) { // return if we already captured it let input = 'touch' if (undefined !== oTemp[input][type]) {return} addEvent(type) oTemp[input][type] = {} try { let touch = event.touches[0] // touchcancel + touchend don't have touch data if (undefined == touch) { touch = event.changedTouches[0] } aList.forEach(function(k){ let value try { value = touch[k] if ('number' !== typeof value) { value = 'err' } else if (Number.isNaN(value)) { value = 'NaN' } oTemp[input][type][k] = value } catch(e) { oTemp[input][type][k] = e.name } }) // display when we have all events let expected = oDataEvents[input].length let count = Object.keys(oTemp[input]).length if (count == expected) { let oDisplay = [], oTmpFP = {} let aStable = [0, 0.5, 1] aList.forEach(function(k){ // if all tests are the same value just display one value let aSet = new Set(), aArray = [] for (const p of Object.keys(oTemp[input]).sort()) { // sort so array is in order when needed let x = oTemp[input][p][k] // force is variable if ('force' == k) {if ('number' == typeof x && !aStable.includes(x)) {x = 'float'}} aSet.add(x) aArray.push(x) } let fp = aArray if (1 == aSet.size) {aArray = Array.from(aSet); fp = aArray[0]} let str = aArray.join(' | ') oDisplay.push(s6 + k.padStart(padlen) +": "+ sc + str) oTmpFP[k] = fp }) // modify the FP by replacing screen/clientX/Y with booleans for valid matches let aCoords = ['X','Y'] aCoords.forEach(function(k){ let isMatch = false, isAValid = true, isBValid = true let A = oTmpFP['client' +k], B = oTmpFP['screen'+ k] let typeA = typeof A, typeB = typeof B if ('object' == typeA) { A.forEach(function(m){if ('number' !== typeof m) {isAValid = false}}) } else { if ('number' !== typeA) {isAValid = false} } if ('object' == typeB) { B.forEach(function(m){if ('number' !== typeof m) {isBValid = false}}) } else { if ('number' !== typeB) {isAValid = false} } if (isAValid && isBValid) {isMatch = mini(A) === mini(B)} // remove delete oTmpFP['client' +k] delete oTmpFP['screen' +k] // add oTmpFP['client_screen_'+ k +'_match'] = isMatch }) // sort FP data into overall FP for (const k of Object.keys(oTmpFP).sort()) {oFP[input][k] = oTmpFP[k]} dom.touch.innerHTML = oDisplay.join("<br>") + '<br><br>' finish() } } catch(e) { dom.touch.innerHTML = e+'<br><br>' } } function run(event, type) { // return if we already captured it let input = 'pointer' if (undefined !== oTemp[input][type]) {return} addEvent(type) oTemp[input][type] = {} // isPrimary: https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/isPrimary // pen/touch can be true or false: as discovered in tests let oEvent = {} for (const k of Object.keys(oList)) { let value try { value = event[k] let expected = oList[k] if (typeof value !== expected) { value = 'err' } else if ("number" == expected && Number.isNaN(value)) { value = 'NaN' } } catch(e) { value = e.name } oEvent[k] = value } // sort for (const k of Object.keys(oEvent).sort()) {oTemp[input][type][k] = oEvent[k]} // display when we have all events let expected = oDataEvents[input].length let count = Object.keys(oTemp[input]).length if (count == expected) { let oDisplay = [], oTmpFP = {} let aStable = [0, 0.5, 1] let lines = ["pressure", "pointerType", "tangentialPressure", "altitudeAngle"] let divider = "<span class='faint'>" + "----------".padStart(padlen) +"--------"+ sc for (const k of Object.keys(oList)) { if (lines.includes(k)) {oDisplay.push(divider)} // if all tests are the same value just display one value let aSet = new Set(), aArray = [] for (const p of Object.keys(oTemp[input]).sort()) { // sort so array is in order when needed let x = oTemp[input][p][k] // active pen pressure is variable between 0-1 if ('pressure' == k || 'mozPressure' == k) { if ('number' == typeof x && !aStable.includes(x)) {x = 'float'} } aSet.add(x) aArray.push(x) } let fp = aArray if (1 == aSet.size) {aArray = Array.from(aSet); fp = aArray[0]} let str = aArray.join(' | ') // tweak the fp width/height for less entropy/stability (e.g touch) // but still show real values in display str if ('width' == k || 'height' == k) { if ('object' == typeof fp) {fp = 'mixed'} } oDisplay.push(s6 + k.padStart(padlen) +": "+ sc + str) oTmpFP[k] = fp } // sort FP data into overall FP for (const k of Object.keys(oTmpFP).sort()) {oFP[input][k] = oTmpFP[k]} dom.results.innerHTML = oDisplay.join("<br>") + '<br><br>' finish() } } function addEvent(type) { setEvents.add(type) let aEvents = Array.from(setEvents) dom.events.innerHTML = aEvents.join(', ') } function reset() { oTemp = {'pointer':{},'touch':{}}, oData = {} oFP.pointer = {} oFP.pointerrawupdate = '' oFP.touch = {} // force mouse out of target area if (isStart) { start() dom.resettext.innerHTML = '1: RESET' isStart = false } else { dom.results = '' dom.touch = '' dom.events = '' dom.raw = '' dom.hash = '' dom.touchhash = '' dom.pointerhash = '' setEvents.clear() setRaw.clear() } } function start() { let target = dom.target // record the event happening for (const k of Object.keys(oNonDataEvents)) { let list = oNonDataEvents[k] list.forEach(function(type){ target.addEventListener(k + type, (event) => {addEvent(k + type)}) }) } // record data on these events for (const k of Object.keys(oDataEvents)) { let list = oDataEvents[k] dom[k +'events'].innerHTML = ' [' + list.join('|') +'] ' list.forEach(function(type){ let str = 'auxclick' == type ? type : k + type if ('pointer' == k) { target.addEventListener(str, (event) => {run(event, str)}) } else if ('touch' == k) { target.addEventListener(str, (event) => {runtouch(event, str)}) } }) } // add an auxclick // tested, this doesn't seem to reveal/leak anything than all the other pointer events //target.addEventListener('auxclick', (event) => {run(event, 'auxclick')}) // pointerrawupdate addEventListener("pointerrawupdate", (event) => { let data = 'undefined' if (event.getCoalescedEvents && event.getCoalescedEvents().length > 1) { console.log("Coalesced events:", event.getCoalescedEvents()); } else { data = event.persistentDeviceId +' '+ event.pointerType } setRaw.add(data) let aRaw = Array.from(setRaw) dom.raw.innerHTML = aRaw.join(', ') }) } function run_once() { let t0 = performance.now() let hash, display = '', data = {'element': [], 'window': []}, counter = 0 // maxtouchpoints try {hash = navigator.maxTouchPoints} catch(e){hash = zErr} oFP.maxTouchPoints = hash dom.maxTouchPoints.innerHTML = hash // keys try { let parser = new DOMParser let doc = parser.parseFromString('<div>', "text/html") let htmlElement = doc.body.firstChild for (const key in htmlElement) { counter++ if (key.includes('Touch') || key.includes('touch')) {data['element'].push(key +', '+ counter)} } if (data['element'].length) { hasKeys = true // when we have element keys we have touch events } else { data['element'] = 'none' } } catch(e) { data['element'] = e.name } // properties let id = 'iframe-window-version' counter = 0 try { // create & append let el = document.createElement('iframe') el.setAttribute('id', id) el.setAttribute('style', 'display: none') document.body.appendChild(el) // get props let iframe = dom[id] let contentWindow = iframe.contentWindow let props = Object.getOwnPropertyNames(contentWindow) props.forEach(function(item){ counter++ if (item.includes('Touch') || item.includes('touch')) {data['window'].push(item + ', ' + counter)} }) //let test = props.filter(x => x.includes('ouch')) //console.log(test) if (data['window'].length == 0) { data['window'] = 'none' } } catch(e) { data['window'] = e.name } removeElementFn(id) //console.log(performance.now() - t0) // hash if (data['element'].length && 'object' == typeof data['element']) {data.element.sort()} if (data['window'].length && 'object' == typeof data['window']) {data['window'].sort()} hash = mini(data) if ('a9139fa7' == hash) {data = 'none'; hash = 'none'} else {display = '<br><br>' + json_stringify(data)} oFP.touch_properties = data dom.touch_properties.innerHTML = hash + display } let targetreset = dom.reset targetreset.addEventListener("pointerdown", (event) => {reset()}) run_once() </script> </body> </html> ================================================ FILE: tests/pointertouchevents.html ================================================ <!doctype html> <html lang="en"> <head> <title>Pointer+Touch event dump</title> </head> <body> <button>Clear</button> <label><input type="checkbox" id="mouse"> Mouse events</label> <label><input type="checkbox" id="move"> Move events</label> <pre></pre> <script> const pre = document.querySelector("pre"); const mouse = document.getElementById("mouse"); const move = document.getElementById("move"); document.querySelector("button").addEventListener("click", () => { pre.textContent = ""; }); function log(t) { pre.textContent = t + "\n" + pre.textContent; } function dumpPointer(e) { if (e.pointerType == "mouse" && !mouse.checked) { return; } if (e.type == "pointermove" && !move.checked) { return; } log(`${e.type} ${e.pointerType} ${e.pointerId} (pressure=${e.pressure}; client=(${e.clientX}, ${e.clientY}) primary=${e.isPrimary})`); } function dumpTouch(e) { if (e.type == "touchmove" && !move.checked) { return; } const touches = Array.from(e.touches ?? [], t => "" + t.identifier).join(" "); log(`${e.type} [${touches}]`); } for (const e of ["cancel", "down", "enter", "leave", "move", "out", "over", "up"]) { document.addEventListener(`pointer${e}`, dumpPointer); } for (const e of ["cancel", "end", "move", "start"]) { document.addEventListener(`touch${e}`, dumpTouch); } </script> </body> </html> ================================================ FILE: tests/pr.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>pr: select</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">pluralrules: select <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="c perf" id="perf1st"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm the minimum set of numbers to return maximum entropy in Intl.PluralRules. The first test checks all numbers from 0 to 102 inclusive. A second test checks only those numbers the first instance we saw them, per option (cardinal, ordinal). An empty custom test will instead run the minimal example</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span class="btn4 btnfirst" onClick="run()">[ run ]</span> <span class="btn4 btn" onClick="clearcustom()">[ clear input ]</span> <br><br> <textarea rows="2" placeholder="" style="width: 98%; resize: vertical" id="custom"></textarea> <br><br><hr> <br><span class="spaces" id="numbers2"></span> <br><span class="spaces" id="results2"></span> <br><span class="spaces" id="numbers1"></span> <br><span class="spaces" id="results1"></span> <br><span class="spaces" id="numbers0"></span> <br><span class="spaces" id="results0"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [] let aFirstCardinal = [], aFirstOrdinal = [], main_buckets = [], useSupportedOnly = true, perf = "", isSupported = false function clearcustom() { dom.custom.value = "" } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.PluralRules.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_test(type) { // vars let t0 = performance.now() let aNosCardinal = [], // numbers used aNosOrdinal = [] let test_all = [], // result hash + locale code + result blinkStr = "", str = "" let element = document.getElementById("results" + type) let elperf = document.getElementById("perf" + type) let aTemp = [] // main test if (type == "0") { aTemp = [] for (let i=0; i < 101; i++) {aTemp.push(i)} aNosCardinal = aTemp aNosOrdinal = aTemp main_buckets = [] } // first changes if (type == "1") { aNosCardinal = aFirstCardinal aNosOrdinal = aFirstOrdinal // FF121: 1859752 ICU 74 : lij // to keep lij unique from it,sc in ordinal (bug?) if (!isFF || "lij" === Intl.Collator.supportedLocalesOf("lij").join()) { if (!aNosOrdinal.includes(81)) { aNosOrdinal.push(81) // chromium } } } // custom if (type == "2") { let value = dom.custom.value let go = false value = value.trim() if (value == "") { // our minimal example aNosCardinal = [0, 1, 2, 3, 7, 21, 100] // redundant: 4, 6, 11, 20 aNosOrdinal = [1, 2, 3, 4, 5, 6, 8, 10, 81] // redundant: 0, 6, 7, 9, 21 || FF147+ we need 6 // display dom.custom.value = "[" + aNosCardinal.join(", ") +"]" + "\n[" + aNosOrdinal.join(", ") +"]" } else { aNosCardinal = [] aNosOrdinal = [] // cardinal let start1 = value.indexOf("["), start2 = value.indexOf("]") let str1 = value.slice(start1+1, start2) aTemp = [] aTemp = str1.split(",") aTemp.forEach(function(item) { item = item.trim() if (item !== "") { item = item * 1 if (Number.isInteger(item)) { aNosCardinal.push(item) } } }) // ordinal value = value.slice(start2+1, value.length) start1 = value.indexOf("["), start2 = value.indexOf("]") let str2 = value.slice(start1+1, start2) aTemp = [] aTemp = str2.split(",") aTemp.forEach(function(item) { item = item.trim() if (item !== "") { item = item * 1 if (Number.isInteger(item)) { aNosOrdinal.push(item) } } }) if (aNosCardinal.length || aNosOrdinal.length) {go = true} } if (go) { // dedupe aNosCardinal = aNosCardinal.filter(function(item, position) {return aNosCardinal.indexOf(item) === position}) aNosOrdinal = aNosOrdinal.filter(function(item, position) {return aNosOrdinal.indexOf(item) === position}) // sort aNosCardinal.sort((a,b) => a-b) aNosOrdinal.sort((a,b) => a-b) } } // always sort the arrays aNosCardinal.sort((a,b) => a-b) aNosOrdinal.sort((a,b) => a-b) if (aNosCardinal.length == 0 && aNosOrdinal == 0) { let header = "CUSTOM TEST" if (type == "1") {header = "<hr><br>FIRST CHANGES ONLY"} document.getElementById("numbers" + type).innerHTML = s4 + header + sc element.innerHTML = "<br>there are no numbers to check<br>" return } for (let j=0; j < aLocales.length; j++) { // reset cardinal let prevC = "", currentC = "", tmp_resultC = [] let c0 = true, c1 = true, c2 = true, c3 = true, c4 = true, c5 = true // reset ordinal let prevO = "", currentO = "", tmp_resultO = [] let o0 = true, o1 = true, o2 = true, o3 = true, o4 = true, o5 = true let code = aLocales[j] let intlPRcardinal = new Intl.PluralRules(code, {type:"cardinal"}) let intlPRordinal = new Intl.PluralRules(code, {type:"ordinal"}) // cardinal for (let k=0; k < aNosCardinal.length; k++) { let n = aNosCardinal[k] try { currentC = intlPRcardinal.select(n) if (type == "0") { // catch first change only: not EVERY change if (c0 && currentC == "zero") {c0 = false; aFirstCardinal.push(n)} if (c1 && currentC == "one") {c1 = false; aFirstCardinal.push(n)} if (c2 && currentC == "two") {c2 = false; aFirstCardinal.push(n)} if (c3 && currentC == "few") {c3 = false; aFirstCardinal.push(n)} if (c4 && currentC == "many") {c4 = false; aFirstCardinal.push(n)} if (c5 && currentC == "other") {c5 = false; aFirstCardinal.push(n)} } } catch(e) { currentC = "error" } // record all changes if (prevC !== currentC) { tmp_resultC.push(n +": "+ currentC) } prevC = currentC } // ordinal for (let k=0; k < aNosOrdinal.length; k++) { let n = aNosOrdinal[k] try { currentO = intlPRordinal.select(n) if (type == "0") { // catch first change only: not EVERY change if (o0 && currentO == "zero") {o0 = false; aFirstOrdinal.push(n)} if (o1 && currentO == "one") {o1 = false; aFirstOrdinal.push(n)} if (o2 && currentO == "two") {o2 = false; aFirstOrdinal.push(n)} if (o3 && currentO == "few") {o3 = false; aFirstOrdinal.push(n)} if (o4 && currentO == "many") {o4 = false; aFirstOrdinal.push(n)} if (o5 && currentO == "other") {o5 = false; aFirstOrdinal.push(n)} } } catch(e) { currentO = "error" } // record all changes if (prevO !== currentO) { tmp_resultO.push(n +": "+ currentO) } prevO = currentO } // array: hash-combined + code + resultC + resultO let hashC = mini(tmp_resultC) let hashO = mini(tmp_resultO) let hashCombined = mini(hashC + hashO) let strC = tmp_resultC.join(", ") let strO = tmp_resultO.join(", ") test_all.push(hashCombined +"~"+ code +"~"+ strC +"~"+ strO) } // clean up first changes if (type == "0") { aFirstCardinal = aFirstCardinal.filter(function(item, position) {return aFirstCardinal.indexOf(item) === position}) aFirstCardinal.sort((a,b) => a-b) aFirstOrdinal = aFirstOrdinal.filter(function(item, position) {return aFirstOrdinal.indexOf(item) === position}) aFirstOrdinal.sort((a,b) => a-b) } if (type == "1" || type == "2") { let typename = (type == "1" ? "first changes only" : "custom test") let target = document.getElementById("numbers"+ type) target.innerHTML = (type == "1" ? "<hr><br>" : "") + s4 + typename.toUpperCase() + sc + "<br><br>"+ s4 +"["+ aNosCardinal.length +"]" + sc + s12 + " cardinal: "+ sc +"["+ aNosCardinal.join(", ") + "] <br>"+ s4 +"["+ aNosOrdinal.length +"]" + sc + s12 + " ordinal: "+ sc +"["+ aNosOrdinal.join(", ") +"]" + blinkStr } // perf if (type == "1") { dom.perf1st.innerHTML = '1st: '+ Math.round(performance.now()-t0) +" ms" } else { dom.perf.innerHTML = Math.round(performance.now()-t0) +" ms" } // sort array & loop: get hash + code + result buckets, and code_total test_all.sort() let bucket_hash = [], bucket_code = [], bucket_resC = [], bucket_resO = [] let tmp_code = [], nextHash = "", code_total = 0 for (let i=0; i < test_all.length; i++) { let part1 = test_all[i].split("~")[0], part2 = test_all[i].split("~")[1], part3 = test_all[i].split("~")[2], part4 = test_all[i].split("~")[3] // build code string tmp_code.push(part2) // grab next item if (i < test_all.length - 1) { nextHash = test_all[(i+1)].split("~")[0] } else { nextHash = "end" } // next hash is diff: write data if (nextHash !== part1) { bucket_hash.push(part1 + s4 +" ["+ tmp_code.length +"]"+ sc) bucket_resC.push(part3) bucket_resO.push(part4) bucket_code.push(tmp_code.join(", ")) code_total += tmp_code.length tmp_code = [] // reset tmp_code } } // main if (type == "0") { // build pretty BEFORE sorting let pretty = [] for (let i=0; i < bucket_hash.length; i++) { let part1 = s4 +"hash: "+ sc + bucket_hash[i] let part2 = "<ul class='main'><li>" + s12 +"cardinal: "+ sc + bucket_resC[i] +"</li>" let part3 = "<li>" + s12 +"ordinal: "+ sc + bucket_resO[i] +"</li>" let part4 = "<li>" + s12 +"locale: "+ sc + bucket_code[i] +"</li></ul>" pretty.push(part1 + part2 + part3 + part4) } // remember main details main_buckets = bucket_code main_buckets.sort() // output str = code_total + (code_total == aLocales.length ? sg : sb) +" [match]"+ sc str = "<hr><br>"+ s4 +"ALL NUMBERS ["+ aNosCardinal.length +"]"+ sc +"<br>" +"<ul class='main'><li>"+ s12 +"unique hashes: "+ sc + s16 + main_buckets.length +sc + "</li>" +"<li>"+ s12 +" locales hash: "+ sc + mini(main_buckets) +"</li>" +"<li>"+ s12 +" locale check: "+ sc + str +"</li></ul>" dom.numbers0.innerHTML = str element.innerHTML = pretty.join("") } // first changes if (type == "1" || type == "2") { // set vars to compare to main bucket_code.sort() let check_count = bucket_code.length let check_hash = mini(bucket_code) let main_hash = mini(main_buckets) // append results let matchbad = sb +" [match]"+ sc, matchgood = sg +" [match]"+ sc str = "<ul class='main'><li>" + s12 +"unique hashes:"+ sc str += " "+ s16 + check_count + sc + (check_count == main_buckets.length ? matchgood : matchbad) str += "</li><li>"+ s12 +" locales hash: "+ sc + check_hash + (check_hash == main_hash ? matchgood : matchbad) str += "</li></ul>" element.innerHTML = str if (check_hash !== main_hash) { let testName = (type == "1" ? "FIRST CHANGES ONLY": "CUSTOM TEST" ) console.debug("MISMATCH\n" + "101 TEST buckets\n", main_buckets, testName +" buckets\n", bucket_code) } } } function run() { if (isSupported) { perf = "" dom.perf = "" dom.numbers2 = "" dom.numbers1 = "" dom.numbers0 = "" dom.results2 = "" dom.results1 = "" dom.results0 = "" // delay so users see change and allow paint setTimeout(function() { run_test("0") // main test run_test("1") // 1st changes only run_test("2") // custom }, 1) } } dom.custom.placeholder = "two arrays [cardinal] then [ordinal] e.g." + "\n[0, 1, , , 7 , 21, 100 ]" + "\n[1,2, 3 , 5, 8, 45 ,non integers ignored ]" Promise.all([ get_globals() ]).then(function(){ buildnav() // support try { let test = new Intl.PluralRules(undefined) isSupported = true } catch(e) { dom.numbers2.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // add additional locales to core locales for this test let aListExtra = [] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) legend() if (isSupported) { setTimeout(function() { run() }, 1) } }) </script> </body> </html> ================================================ FILE: tests/prrange.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>pr: selectrange</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">pluralrules: selectrange <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy. Both tests are not "equal": ALL calculates every single combination of words (using all numbers) which creates an exact number of hashes in <code>selectRange</code>. Languages that share selectRanges can differ in <code>select</code> (see previous test) and MIN uses minimal numbers, which means languages that share selectRange can now end up with different word combos, creating more differences. The key is for MIN to match ALL, not exceed it. </span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <span id="maxsettings"> <input type="checkbox" name="maximum" style="margin: 0; height: 12px"> <span class="no_color">test 0-100 (slow... 3-5 secs)</span> </span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], oLocaleChanges = {}, oChanges = {}, aAll = [], isMax = false, isSupported = false, localesHashAll = "" // to compare min to function generatePairs(max){ aAll = [] for (let i=0; i <= max; i++) { for (let j=0; j <= max; j++) { aAll.push([i, j]) } } } function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.PluralRules.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() let spacer = "<br><br>" // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/selectRange /* using generated pairs _should_ catch all combos of "words" NOTE: blink is very slow and may prompt to kill the script and can hang: tested 100 cardinal but will set at 21 for PoC 100 = 30 unique hashes 21 = 30 https://github.com/unicode-org/cldr/blob/main/common/supplemental/pluralRanges.xml */ let testC = aAll, testO = aAll if (method == 'min') { //test = [[0,1],[0,2],[1,1],[2,2]] // 20 - but not the same locale groupings // 50 + 33 items from oChanges (deduped all number pairs per pr option) testC = [ [0,0],[0,1],[0,2],[0,3],[0,4], [0,6],[0,7],[0,11],[0,20], [1,0],[1,1],[1,2],[1,3],[1,4],[1,5],[1,6],[1,7],[1,11],[1,20], [2,0],[2,1],[2,2],[2,3],[2,4],[2,5],[2,6],[2,7],[2,11], [3,0],[3,1],[3,2],[3,3], [3,5],[3,6],[3,7],[3,11], [4,1],[4,2],[4,3],[4,4],[4,5],[4,6],[7,7],[7,11], [11,0],[11,3],[11,11], [20,0],[20,1],[20,20], ] testO = [ [0,0],[0,1],[0,2],[0,3],[0,5],[0,6], [1,1],[1,2],[1,3],[1,5],[1,6],[1,7],[1,11],[1,21], [2,2],[2,3],[2,4],[2,5],[2,7], [3,3],[3,4],[3,5],[3,7], [9,1],[9,3],[9,6],[9,9], [10,1],[10,2],[10,3],[10,5],[10,7], [11,0], ] // let's play // ok, what am I missing: I can get more than 30 if I exclude some of the 50/33 testC = [[0,0],[1,1],[2,1],[2,4],] testO = [[0,0],[0,1],[0,6],[1,1],[1,3],[1,5],[3,3],] } let oData = {}, oTempData = {}, oChanges = {} // reset oLocaleChanges = {'C': {}, 'O': {}} try { aLocales.forEach(function(code) { let oTmp = {'C': {}, 'O': {}}, prevC = '', prevO = '' // cardinal let formatterC = new Intl.PluralRules(code, {type:'cardinal'}) oLocaleChanges['C'][code] = [] testC.forEach(function(t){ let rC = formatterC.selectRange(t[0], t[1]) if (rC !== prevC) { let keyC = formatterC.select(t[0]) +'-'+ formatterC.select(t[1]) if (oTmp['C'][keyC] == undefined) { oTmp['C'][keyC] = rC oLocaleChanges['C'][code].push([t[0], t[1]]) // changes } prevC = rC } }) // ordinal let formatterO = new Intl.PluralRules(code, {type:'ordinal'}) oLocaleChanges['O'][code] = [] testO.forEach(function(t){ let rO = formatterO.selectRange(t[0], t[1]) if (rO !== prevO) { let keyO = formatterO.select(t[0]) +'-'+ formatterO.select(t[1]) // rules are the same between cardinal and ordinal // we test ordinal because it gioves us more "words", e.g. "two" in hebrew if (oTmp['O'][keyO] == undefined) { oTmp['O'][keyO] = rO oLocaleChanges['O'][code].push([t[0], t[1]]) // changes } prevO = rO } }) // sort oTmp by keys let newObj = {} for (const k of Object.keys(oTmp).sort()) { newObj[k] = {} for (const j of Object.keys(oTmp[k]).sort()) { newObj[k][j] = oTmp[k][j] } } // hash + add let hash = mini(newObj) if (oTempData[hash] == undefined) { oTempData[hash] = {'data': newObj, 'locales': [code]} } else { oTempData[hash].locales.push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +': '+ sc + ' try again' return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == 'locales') { oData[n][p] = oTempData[n][p].join(', ') } else { oData[n][p] = oTempData[n][p] } } } //console.log(oData) let localeGroups = [], displaylist = [] for (const hash of Object.keys(oData)) { localeGroups.push(oData[hash]['locales']) let localeCount = oData[hash]['locales'].split(',').length let str = '' for (const p of Object.keys(oData[hash])) { if (p !== 'locales') { for (const type of Object.keys(oData[hash][p])) { // type: C(ardinal) or O(rdinal) let aTmp = [] for (const key of Object.keys(oData[hash][p][type])) { aTmp.push(s99 + key + ': '+ sc + oData[hash][p][type][key]) } str += '<li>'+ s14 + (type == 'C' ? 'cardinal' : 'ordinal') + sc +'<br>' + aTmp.join('<br>') +'</li>' } } } displaylist.push( s12 + hash + sc + s4 + ' ['+ localeCount +']'+ sc + "<ul class='main'>"+ str + '<li>'+ s12 +'L: '+ sc + oData[hash]['locales'] +'</li></ul>' ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all" & !isMax) { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "fd71342c") { // FF147+ } else if (resultsHash == "deeeb2ad") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "ce9ce45f") { // FF147+: 30 } else if (localesHash == "d3b22832") { // FF140-146: 30 } else {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let countC = testC.length, countO = testO.length let testCount = 'all' == method ? countC*2 : (countC + countO) +': '+ testC.length +' cardinal + '+ testO.length +' ordinal' let testInfo = '', testRange = '' if (isFF && 'all' == method) { testRange = 'nos: 0-'+ (isMax ? '100' : '21') +' | ' } testInfo = ' [' + testRange + 'tests: '+ testCount + ']' let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + testInfo + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" // unique changes //console.log(oLocaleChanges) for (const k of Object.keys(oLocaleChanges)) { // cardinal/ordinal let tmpChanges = [], strChanges = '' oChanges[k] = [] for (const l of Object.keys(oLocaleChanges[k])) { // each locale for (const a of Object.keys(oLocaleChanges[k][l])) { // each array tmpChanges.push(oLocaleChanges[k][l][a].join('-')) } } // dedupe tmpChanges = tmpChanges.filter(function(item, position) {return tmpChanges.indexOf(item) === position}) tmpChanges.forEach(function(c) { let parts = c.split('-') oChanges[k].push([parts[0]*1, parts[1]*1]) strChanges += '['+ parts[0] +','+ parts[1]+'],' }) oChanges[k +'_string'] = strChanges } //console.log(oChanges) } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" if ('all' == method) { isMax = dom.maximum.checked let max = isMax ? 100 : 21 generatePairs(max) } // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() dom.maximum.checked = false if (!isFF) { dom.maxsettings.style.display = 'none' } try { let test = new Intl.PluralRules(undefined) isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } let aListExtra = [] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) // test //list = ['de,german','en,english',] //list = ['uk,ukrainian',] legend() if (isSupported) { generatePairs(21) // default setBtn("all") setTimeout(function() { run_main("all") }, 100) } }) </script> </body> </html> ================================================ FILE: tests/readerview.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>reader view</title> <style> body { background-color: #fae5df; width: 95%; min-width: 380px; max-width: 680px; } h1 {color: #5f2c3e; font-weight: bold;} p {color: #ed5841;} .center { text-align: center; } </style> </head> <body> <article> <h1 id="test">Lorem Ipsum</h1> <p class="center"><a class="return" href="../index.html#other">return to TZP index</a></p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Massa tincidunt nunc pulvinar sapien et. Ultricies tristique nulla aliquet enim. Ac tortor dignissim convallis aenean et tortor at risus viverra. Tellus at urna condimentum mattis pellentesque. Lorem ipsum dolor sit amet consectetur adipiscing elit. Nunc mi ipsum faucibus vitae. Nibh cras pulvinar mattis nunc sed blandit libero volutpat sed. Arcu non sodales neque sodales ut etiam sit. Porta lorem mollis aliquam ut porttitor leo a. Egestas integer eget aliquet nibh. Morbi tincidunt ornare massa eget egestas purus viverra accumsan. A cras semper auctor neque. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus non. Sapien et ligula ullamcorper malesuada proin libero nunc.</p> <p>Augue eget arcu dictum varius duis at consectetur. Pellentesque dignissim enim sit amet venenatis. Magna etiam tempor orci eu lobortis. Gravida neque convallis a cras semper auctor neque. Quis risus sed vulputate odio. Faucibus et molestie ac feugiat sed lectus vestibulum. Feugiat scelerisque varius morbi enim nunc faucibus a. Netus et malesuada fames ac turpis egestas integer eget. Pellentesque id nibh tortor id aliquet lectus proin nibh nisl. Adipiscing commodo elit at imperdiet dui accumsan sit. Nulla pharetra diam sit amet nisl suscipit adipiscing bibendum est. Dignissim cras tincidunt lobortis feugiat vivamus at augue eget. Tristique nulla aliquet enim tortor at auctor urna nunc. Integer enim neque volutpat ac. Viverra suspendisse potenti nullam ac. Volutpat odio facilisis mauris sit amet.</p> <p>Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. Pellentesque elit ullamcorper dignissim cras. Nulla facilisi cras fermentum odio eu feugiat pretium. Tortor posuere ac ut consequat semper viverra nam. Ridiculus mus mauris vitae ultricies leo integer malesuada nunc. Convallis a cras semper auctor neque. Eu facilisis sed odio morbi quis. Orci dapibus ultrices in iaculis nunc sed augue. Neque sodales ut etiam sit amet nisl purus in mollis. Interdum consectetur libero id faucibus nisl tincidunt. Vitae auctor eu augue ut lectus. Tristique risus nec feugiat in. Vitae congue eu consequat ac felis. Fermentum leo vel orci porta non pulvinar. Tempus iaculis urna id volutpat lacus laoreet non curabitur. Quam adipiscing vitae proin sagittis nisl rhoncus mattis.</p> </article> </body> </html> ================================================ FILE: tests/recursion.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>recursion</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <style> table {width: 97%; min-width: 380px; max-width: 480px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#devices">return to TZP index</a></td></tr> </table> <table id="tb7"> <col width="40%"><col width="60%"> <thead><tr><th colspan="2"> <div class="nav-title">recursion | stack length <div class="nav-up"><span class="c perf" id="perf"></span></div> <div class="nav-down"><span class="c perf" id="type"></span></div> </div> </th></tr></thead> <tr> <td class="padr"><span class="btnfirst btn" onClick="run('worker')">[ run ]</span> WORKER</td> <td><span class='mono faint'>level: <span id="worker1"></span><br>stack: <span id="worker2"></span></span></td> </tr> <tr> <td class="padr"><span class="btnfirst btn" onClick="run('doc')">[ run ]</span> DOCUMENT</td> <td><span class='mono faint'>level: <span id="doc1"></span><br>stack: <span id="doc2"></span></span></td> </tr> <tr> <td class="padr"><span class="btnfirst btn" onClick="run('iframe')">[ run ]</span> IFRAME</td> <td id="iframehere" class="mono faint"></td> </tr> </table> <br> <script> 'use strict'; function perf(t0) { document.getElementById("perf").innerHTML = Math.round(performance.now() - t0) +" ms" } const get_isRecursion_doc = () => new Promise(resolve => { let t0 = performance.now() try { let level = 0 function recurse() {level++; recurse()} try {recurse()} catch(e) {} level = 0 try { recurse() } catch(e) { // 2nd test is more accurate/stable document.getElementById("doc1").innerHTML = level document.getElementById("doc2").innerHTML = e.stack.toString().length perf(t0) return resolve() } } catch(e) { console.error(e) document.getElementById("doc1").innerHTML = e.name return resolve() } }) function get_isRecursion_iframe() { let t0 = performance.now() let iframe let id = "targetiframe" try { // create & append let el = document.createElement("iframe") el.setAttribute("id", id) el.width = "200" el.setAttribute("style", "border: none") const node = document.getElementById("iframehere") node.appendChild(el) // add iframe iframe = document.getElementById(id) iframe.addEventListener("error", event => { document.getElementById("iframehere").innerHTML = event.type }) iframe.onload = function() { // slow AF } iframe.src = "recursion_iframe.html" } catch(e) { try {iframe.parentNode.removeChild(iframe)} catch(err) {} document.getElementById("iframehere").innerHTML = e.name } } const get_isRecursion_worker = () => new Promise(resolve => { let t0 = performance.now() const METRIC = "isRecursion" try { let worker = new Worker("recursion_worker.js") worker.addEventListener("message", function(msg) { for (const k of Object.keys(msg.data)) { document.getElementById(k).innerHTML = msg.data[k] } perf(t0) worker.terminate }, false) worker.postMessage("") } catch(e) { console.error(e) document.getElementById("worker1").innerHTML = e.name } }) function run(type) { document.getElementById("type").innerHTML = (type == "doc" ? "document" : type) document.getElementById("perf").innerHTML = "" if (type == "iframe") { // remove iframe let iframe = document.getElementById("targetiframe") try {iframe.parentNode.removeChild(iframe)} catch(err) {} } else { for (let i=1; i < 3; i++) { try { document.getElementById(type+i).innerHTML = "" } catch(e) {} } } // delay setTimeout(function() { if (type == "doc") { get_isRecursion_doc() } else if (type == "worker") { get_isRecursion_worker() } else if (type == "iframe") { get_isRecursion_iframe() } }, 100) } </script> </body> </html> ================================================ FILE: tests/recursion_iframe.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="recursion" content="width=300"> <style> .s14 {color: #b29ddc;} .s18 {color: #dc9db2;} .faint {color: #808080;} .mono {font-family: monospace, "Courier New"; font-size: 12px;} .spaces {white-space: pre-wrap;} .top { position: fixed; top: 0px; left: 0px; } </style> </head> <body> <div class="top mono"> <span class='indent faint'>level: <span id="doc1"></span><br>stack: <span id="doc2"></span></span> </div> </body> <script> 'use strict'; const iframe_function = () => new Promise(resolve => { try { let level = 0 function recurse() {level++; recurse()} try {recurse()} catch(e) {} level = 0 try { recurse() } catch(e) { document.getElementById("doc1").innerHTML = level document.getElementById("doc2").innerHTML = e.stack.toString().length return resolve() } } catch(e) { console.error(e) document.getElementById("doc1").innerHTML = e.name return resolve() } }) iframe_function() </script> </html> ================================================ FILE: tests/recursion_worker.js ================================================ 'use strict'; addEventListener("message", function(e) { let data = { } try { let level = 0 function recurse() {level++; recurse()} try {recurse()} catch(e) {} level = 0 try { recurse() } catch(e) { data["worker1"] = level data["worker2"] = e.stack.toString().length self.postMessage(data) } } catch(e) { console.error(e) data["worker1"] = e.name self.postMessage(data) } }, false) ================================================ FILE: tests/resolvedoptions.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>resolvedoptions</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="250px"> <thead><tr><th colspan="2"> <div class="nav-title">resolvedoptions <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Max entropy in <code>resolvedOptions()</code> properties across Intl constuctors</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], oData = {}, oTests = {}, localesHashAll = "" // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let test = Intl.DateTimeFormat.supportedLocalesOf([code]) if (test.length) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function run_main(method) { let t0 = performance.now() oData = {} let oTempData = {} let spacer = '<br><br>' function get_metrics(code) { let oTest = {} for (const k of Object.keys(oTests)) { oTest[k] = {} let metrics = oTests[k] try { // set constructor let constructor if ('collator' == k) {constructor = Intl.Collator(code).resolvedOptions() } else if ('datetimeformat' == k) {constructor = Intl.DateTimeFormat(code).resolvedOptions() } else if ('durationformat' == k) {constructor = new Intl.DurationFormat(code).resolvedOptions() } else if ('listformat' == k) {constructor = new Intl.ListFormat(code).resolvedOptions() } else if ('numberformat' == k) {constructor = new Intl.NumberFormat(code).resolvedOptions() } else if ('pluralrules' == k) {constructor = new Intl.PluralRules(code).resolvedOptions() } else if ('relativetimeformat' == k) {constructor = new Intl.RelativeTimeFormat(code).resolvedOptions() } else if ('segmenter' == k) {constructor = new Intl.Segmenter(code).resolvedOptions() } // get values metrics.forEach(function(m) { try { let value if ('hourcycle' == m) { value = Intl.DateTimeFormat(code, {hour: "numeric"}).resolvedOptions().hourCycle } else if ('pluralCategories' == m) { value = constructor[m].join(', ') } else { value = constructor[m] } oTest[k][m] = value } catch(e) { oTest[k][m] = zErr } }) } catch(e) { oTest[k] = zErr } } return oTest } let oMin = { 'collator': ['caseFirst'], 'datetimeformat': ['calendar','day','hourcycle','month','numberingSystem'], 'pluralrules': ['pluralCategories'], } let oMax = { 'collator': ['caseFirst','collation','ignorePunctuation','numeric','sensitivity','usage'], "datetimeformat": ['calendar','day','hourcycle','month','numberingSystem','year'], /* NOTE: displaynames: https://tc39.es/ecma402/#intl-displaynames-objects - four types: currency, language, region, script - e.g. new Intl.DisplayNames(code, {type: 'language'}).resolvedOptions() - second options parameter - fallback: default is code - style: default is long - languageDisplay (for use with type 'language'): default is dialect - so nothing is gained here as all codes are indentical - but we can use this API to get results per locale using 'of' e.g. > let l = new Intl.DisplayNames(code, {type: 'language'}).resolvedOptions() > s.of('en-US') = "American English" // code: en-US > s.of('en-US') = "anglais américain" // code: fr > and we can of course pass undefined as the code */ 'durationformat': [ // creates 10 buckets on it's own 'days','daysDisplay','hours','hoursDisplay','microseconds','microsecondsDisplay', 'milliseconds','millisecondsDisplay','minutes','minutesDisplay','months','monthsDisplay', 'nanoseconds','nanosecondsDisplay','numberingSystem','seconds','secondsDisplay', 'style','weeks','weeksDisplay','years','yearsDisplay' ], /* listformat: https://tc39.es/ecma402/#listformat-objects - options are defaults - style = long, type = conjunction - i.e user must set them */ 'numberformat': [ 'maximumFractionDigits','minimumFractionDigits','minimumIntegerDigits', 'roundingIncrement','roundingMode','roundingPriority','trailingZeroDisplay', 'notation','numberingSystem','signDisplay','style','useGrouping', ], 'pluralrules': [ 'maximumFractionDigits','minimumFractionDigits','minimumIntegerDigits','pluralCategories', 'roundingIncrement','roundingMode','roundingPriority','trailingZeroDisplay','type' ], 'relativetimeformat': ['numberingSystem','numeric','style'], 'segmenter': ['granularity'], } try { oTests = method == 'all' ? oMax : oMin let aHashes = [] aLocales.forEach(function(code) { let oTest = get_metrics(code) let hash = mini(oTest) if (oTempData[hash] == undefined) { aHashes.push(hash) oTempData[hash] = { "data": {}, "locales": [] } for (const k of Object.keys(oTest).sort()) { oTempData[hash]["data"][k] = oTest[k] } } oTempData[hash].locales.push(code) }) // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = oTempData[n] } // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" //console.log(oData) let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let localeCount = oData[k].locales.length let str = "" for (const j of Object.keys(oData[k].data).sort()) { str += s14 + j + sc // +": "+ oData[k].data[j] +"</li>" for (const m of Object.keys(oData[k].data[j]).sort()) { str += "<li>"+ s99 + m + sc +": "+ oData[k].data[j][m] +"</li>" } } // wrap into details for long lists if ('all' == method) {str = "<details><summary>details</summary>"+ str +"</details>"} displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc +"<br>" + "<ul class='main'>"+ str + "<li>"+ s12 +"L: "+ sc + oData[k].locales.join(", ") +"</li></ul>" ) } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 140+ if (isVer > 139) { // results if (resultsHash == "b84cb93a") { // FF147+ } else if (resultsHash == "0af83683") { // FF140-14 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "44faf04f") { // FF147+: 59 } else if (localesHash == "37fee972") { // FF140-146: 59 } else {localesHash += ' '+ zNEW } } } else { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + Object.keys(oData).length + sc +" from "+ s4 + aLocales.length + sc + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ get_isVer() buildnav() // add additional locales to core locales for this test let aListExtra = [ "ar-ae,arabic (united arabic emirates)", "ar-il,arabic (israel)", "ar-sa,arabic (saudi arabia)", "ckb-ir,central kurdish (iran)", "en-nz,english (new zealand)", "es-pr,spanish (puerto rico)", "es-us,spanish (united states)", "ff-adlm,fulah (adlam)", "ff-adlm-gh,fulah (adlam ghana)", "fr-dz,french (algeria)", "lrc-iq,northern luri (iraq)", "ps-pk,pashto (pakistan)", // blink 'ar-bh,arabic (bahrain)', 'ar-ma,arabic (morocco)', 'pa-arab,punjabi (arabic)', 'ur-in,urdu (india)', 'uz-af,uzbek (afghanistan)', 'yo-bj,yoruba (benin)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ["en,english"] legend() setBtn("all") setTimeout(function() { run_main("all") }, 100) }) </script> </body> </html> ================================================ FILE: tests/rtf.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>relativetimeformat</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 680px;} ul.main {margin-left: -20px;} </style> </head> <body> <div class="offscreen"> <div id="test95a" style="width: min-content; hyphens: auto; border: 1px solid red">2020-1</div> <div id="test95b" style="width: min-content; hyphens: auto; border: 1px solid red">2020-12020-1</div> </div> <div class="hidden"> <div><input type="time" min="14:00:00" max="12:00:00" value="15:00:00" id="test76"></div> </div> <table> <col width="25%"><col width="50%"><col width="25%"> <tr><td></td><td colpsan="3"><h2>TorZillaPrint</h2></td><td></td></tr> <tr> <td id="navprev" style="text-align: left;"></td> <td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td> <td id="navnext" style="text-align: right;"></td> </tr> </table> <table id="tb4"> <col width="200px"> <thead><tr><th colspan="2"> <div class="nav-title">relativetimeformat <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof to confirm minimum tests for maximum entropy: from <code>2</code> numeric options (auto, always), <code>3</code> styles (narrow, short, long), <code>8</code> time units (second, minute, hour, day, week, month, year, quarter) and <code>5</code> amounts (-3, -1, 0, 1, 3).</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="bnarrow" class="btn4 btnfirst" onClick="run('narrow')">[ N ]</span> <span id="bshort" class="btn4 btn" onClick="run('short')">[ S ]</span> <span id="blong" class="btn4 btn" onClick="run('long')">[ L ]</span> <span id="ball" class="btn4 btn" onClick="run('all')">[ ALL ]</span> <span id="bmin" class="btn4 btn" onClick="run('min')">[ MIN ]</span> <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = gLocales, aLegend = [], aLocales = [], isSupported = false, localesHashAll = "" // to compare min to function log_console(name) { let hash = mini(sDetail[name]) if (name == "locales") { console.log(name +": " + hash +"\n"+ sDetail["locales"].join("\n")) } else { console.log(name +": " + hash +"\n", sDetail[name]) } } function legend() { // build once if (aLegend.length == 0) { list.sort() for (let i = 0 ; i < list.length; i++) { let str = list[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' let go = true if (isSupported) { go = Intl.RelativeTimeFormat.supportedLocalesOf([code]).length > 0 } if (go) { aLocales.push(code) let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } aLegend.push(code.padStart(7) +": "+ name) } } } // output let header = s4 +"LEGEND ["+ aLegend.length +"]"+ sc +"<br><br>" dom.legend.innerHTML = header + aLegend.join("<br>") } function get_dayperiod(date, code, option) { // always use h12 return new Intl.DateTimeFormat(code, {hourCycle: "h12", dayPeriod: option}).format(date) } function run_main(method) { let t0 = performance.now() let oData = {}, oTempData = {} let spacer = "<br><br>" try { let styles = ['narrow','short','long'] let times = [-3,-1,0,1,3] let units = ["second","minute","hour","day","week","month","quarter","year"] let minList = [] let oOptions = {} // select what to test if (method == "min") { styles = ["narrow","short","long"] // TZP: en-US // A: now, in 1s, in 1 second, in 3s, in 1d, in 3d, this qtr., in 0y // B: today, tomorrow, next wk., next yr. /* ALWAYS N: in 1d | in 0y AUTO N: now | in 1s | in 3s | today | tomorrow | in 3d | next wk. | this qtr. | next yr. L: in 1 second */ // FF65-67 & FF96-110+ minList = [ "autonarrow0second", // A: now "autonarrow1second", // A: in 1s "autolong1second", // A: in 1 second "autonarrow3second", // A: in 3s "autonarrow0day", // B: today "autonarrow1day", // B: tomorrow "alwaysnarrow1day", // A: in 1d "autonarrow3day", // A: in 3d "autonarrow1week", // B: next wk. "autonarrow0quarter", // A: this qtr. "alwaysnarrow0year", // A: in 0y "autonarrow1year", // B: next yr. ] // older versions need some help if (isVer > 67 && isVer < 91) { minList.push("autolong1week") // FF68-90 } if (isVer > 71 && isVer < 96) { minList.push("autoshort-1week") // FF72-95 } /* troubleshooting list "autonarrow-3second", "autonarrow-1second", "autonarrow0second", "autonarrow1second", "autonarrow3second", "autonarrow-3minute", "autonarrow-1minute", "autonarrow0minute", "autonarrow1minute", "autonarrow3minute", "autonarrow-3hour", "autonarrow-1hour", "autonarrow0hour", "autonarrow1hour", "autonarrow3hour", "autonarrow-3day", "autonarrow-1day", "autonarrow0day", "autonarrow1day", "autonarrow3day", "autonarrow-3week", "autonarrow-1week", "autonarrow0week", "autonarrow1week", "autonarrow3week", "autonarrow-3month", "autonarrow-1month", "autonarrow0month", "autonarrow1month", "autonarrow3month", "autonarrow-3year", "autonarrow-1year", "autonarrow0year", "autonarrow1year", "autonarrow3year", "autonarrow-3quarter", "autonarrow-1quarter", "autonarrow0quarter", "autonarrow1quarter", "autonarrow3quarter", */ sDetail["minlist"] = minList } else { if (method == "narrow") {styles = ["narrow"]} if (method == "short") {styles = ["short"]} if (method == "long") {styles = ["long"]} styles.forEach(function(s){ units.forEach(function(u){ oOptions[s + u] = true }) }) } //test: 3 styles x 5 times x 8 units = 120 results aLocales.forEach(function(code) { let oStyles = { "narrowalways": [], "narrowauto": [], "shortalways": [], "shortauto": [], "longalways": [], "longauto": [] } styles.forEach(function(s){ let IntlRTF = new Intl.RelativeTimeFormat(code, {style: s, numeric: "auto"}) let IntlRTFn = new Intl.RelativeTimeFormat(code, {style: s, numeric: "always"}) units.forEach(function(u){ times.forEach(function(t){ if (method == "min") { let concat = s + t + u if (minList.includes("always"+concat)) { oStyles[s +"always"].push( IntlRTFn.format(t, u) ) } if (minList.includes("auto"+concat)) { oStyles[s +"auto"].push( IntlRTF.format(t, u) ) } } else { let concat = s+t+u //if (code == "en") {console.log(concat)} oStyles[s +"always"].push( IntlRTFn.format(t, u) ) oStyles[s +"auto"].push( IntlRTF.format(t, u) ) } }) }) }) let hash = mini(oStyles) +" " // make numbers sort like strings if (oTempData[hash] == undefined) { oTempData[hash] = {} oTempData[hash]["locales"] = [code] oTempData[hash]["longalways"] = oStyles["longalways"].join(" | ") oTempData[hash]["longauto"] = oStyles["longauto"].join(" | ") oTempData[hash]["shortalways"] = oStyles["shortalways"].join(" | ") oTempData[hash]["shortauto"] = oStyles["shortauto"].join(" | ") oTempData[hash]["narrowalways"] = oStyles["narrowalways"].join(" | ") oTempData[hash]["narrowauto"] = oStyles["narrowauto"].join(" | ") } else { oTempData[hash]["locales"].push(code) } }) // handle empty if (Object.keys(oTempData).length == 0) { dom.results.innerHTML = s4 + method.toUpperCase() +": " + sc + " try again" return } // order object for (const n of Object.keys(oTempData).sort()) { oData[n] = {} for (const p of Object.keys(oTempData[n]).sort()) { if (p == "locales") { oData[n][p] = oTempData[n][p].join(", ") } else { oData[n][p] = oTempData[n][p] } } } let localeGroups = [], displaylist = [] for (const k of Object.keys(oData)) { localeGroups.push(oData[k]["locales"]) let alwaysN = oData[k]["narrowalways"], alwaysS = oData[k]["shortalways"], alwaysL = oData[k]["longalways"] let autoN = oData[k]["narrowauto"], autoS = oData[k]["shortauto"], autoL = oData[k]["longauto"] let localeCount = oData[k]["locales"].split(",").length if (alwaysN.length) {alwaysN = "<li>"+ s16 +"N: "+ sc + alwaysN +"</li>"} if (alwaysS.length) {alwaysS = "<li>"+ s16 +"S: "+ sc + alwaysS +"</li>"} if (alwaysL.length) {alwaysL = "<li>"+ s16 +"L: "+ sc + alwaysL +"</li>"} if (autoN.length) {autoN = "<li>"+ s16 +"N: "+ sc + autoN +"</li>"} if (autoS.length) {autoS = "<li>"+ s16 +"S: "+ sc + autoS +"</li>"} if (autoL.length) {autoL = "<li>"+ s16 +"L: "+ sc + autoL +"</li>"} let strAlways = "", strAuto = "" if (alwaysN.length + alwaysS.length + alwaysL.length > 0) { strAlways = s12 +"ALWAYS"+ sc + alwaysN + alwaysS + alwaysL } if (autoN.length + autoS.length + autoL.length > 0) { strAuto = s12 + "AUTO"+ sc + autoN + autoS + autoL } if (method == "all") { displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>" + "<details><summary>details</summary>"+ strAlways + strAuto + "<li>"+ "</details>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } else { displaylist.push( s12 + k + sc + s4 + " ["+ localeCount +"]"+ sc + "<ul class='main'>" + strAlways + strAuto + "<li>"+ s12 +"L: "+ sc + oData[k]["locales"] +"</li></ul>" ) } } // hashes + btns sDetail["results"] = oData let resultsBtn = "<span class='btn4 btnc' onClick='log_console(`results`)'>[details]</span>" let resultsHash = mini(oData) localeGroups.sort() sDetail["locales"] = localeGroups let localesBtn = "<span class='btn4 btnc' onClick='log_console(`locales`)'>[details]</span>" let localesHash = mini(localeGroups) let minBtn = "" if (method == "min") { minBtn = "<span class='btn12 btnc' onClick='log_console(`minlist`)'>[" + minList.length + " tests]</span>" } /* console.log(localesHash) console.log(localeGroups) console.log(resultsHash) console.log(oData) console.log(oTempData) //*/ // notations let localesMatch = "" if (method == "all") { localesHashAll = localesHash // notate new if 128+ if (isVer > 139) { // ignore if non-supported used, which return same as undefined = user's resolved options // results if (resultsHash == "afdf39f2") { // FF147+ } else if (resultsHash == "cba6cd42") { // FF140-146 } else {resultsHash += ' '+ zNEW } // locales if (localesHash == "e2f93418") { // FF147+: 256 } else if (localesHash == "0e9d33f9") { // FF140-14: 250 } else if (isFF) {localesHash += ' '+ zNEW } } } else if (method == "min") { localesMatch = localesHash == localesHashAll ? green_tick : red_cross } // display let display = s4 + method.toUpperCase() +": " +" ["+ localeGroups.length + sc +" from "+ s4 + aLocales.length +"]" + sc + " " + minBtn + spacer + s16 +"results: "+ sc + resultsHash +" " + resultsBtn +"<br>" + s12 +"locales: "+ sc + localesHash +" "+ localesBtn + localesMatch + spacer dom.results.innerHTML = display + "<br>" + displaylist.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch(e) { dom.results.innerHTML = s4 + e.name +": "+ sc + e.message } } function run(method) { if (isSupported) { //reset setBtn(method) dom.perf = "" dom.results = "" // delay so users see change and allow paint setTimeout(function() { run_main(method) }, 1) } } function setBtn(method) { // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } Promise.all([ get_globals() ]).then(function(){ Promise.all([ get_is95(), get_isVer(), buildnav() ]).then(function(){ try { let test = new Intl.RelativeTimeFormat(undefined, {style: "short", numeric: "auto"}) isSupported = true } catch(e) { dom.results.innerHTML = s4 + e.name +":" + sc +" "+ e.message } // add additional locales to core locales for this test let aListExtra = [ 'ar-ae,arabic (united arabic emirates)', 'ar-dj,arabic (djibouti)', 'bs-cyrl,bosnian (cyrillic)', 'en-at,english (austria)', 'en-au,english (australia)', 'en-ca,english (canada)', 'en-sg,english (singapore)', 'es-ar,spanish (argentina)', 'es-co,spanish (colombia)', 'es-mx,spanish (mexico)', 'es-py,spanish (paraguay)', 'es-us,spanish (united states)', 'ff-adlm,fulah (adlam)', 'fr-ca,french (canada)', 'hi-latn,hindi (latin)', 'kk-cn,kazakh (china)', 'kok-latn,konkani (latin)', 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'pa-pk,punjabi (pakistan)', 'ps-pk,pashto (pakistan)', 'pt-gw,portuguese (guinea-bissau)', 'pt-pt,portuguese (portugal)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'shi-latn,tachelhit (latin)', 'sr-ba,serbian (bosnia & herzegovina)', 'sr-latn,serbian (latin)', 'sr-latn-ba,serbian (latin bosnia & herzegovina)', 'ur-in,urdu (india)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'vai-latn,vai (latin)', 'yo-bj,yoruba (benin)', 'yue-hans,cantonese (simplified)', ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) //list = ['en'] legend() if (isSupported) { setBtn('all') setTimeout(function() { run_main('all') }, 100) } }) }) </script> </body> </html> ================================================ FILE: tests/sanitizing.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=700"> <title>sanitizing</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#storage">return to TZP index</a></td></tr> </table> <table id="tb6"> <col width="37%"><col width="63%"> <thead><tr><th colspan="2">sanitizing</th></tr></thead> <tr><td colspan="2" class="intro"><span class="no_color">Checks for specific existing persistent local web storage entries. If no such data is found, it creates it. If at least one is found, it will propagate to any others that do not exist.</span> <br><br><hr> </td></tr> <tr><td class="padr"><div> <div class="btn-left"><span class="btn6 btn" onClick="run_sanitize_check()">[ re-run ]</span></div> <div>[navigator] cookieEnabled</div></div></td><td class="c" id="ckieE"></td> </tr> <tr><td class="padr">[session] JS 1st party cookie</td><td class="faint" id="ckieS"></td></tr> <tr><td class="padr">[persistent] JS 1st party cookie</td><td class="c" id="ckieP"></td></tr> <tr><td class="padr">localStorage</td><td class="c" id="ls"></td></tr> <tr><td class="padr">indexedDB</td><td class="c" id="idb"></td></tr> <tr><td class="padr">service worker cache</td><td class="faint" id="swc"></td></tr> <tr><td colspan="2" class="center">------</td></tr> <tr><td class="padr">debugging</td><td class="c" id="debug"></td></tr> </table> <br> <script> 'use strict'; var ckie = false, // are these storage mechanisms actually working ls = false, idb = false, cookieStored = "", // remember any existing values lsStored = "", idbStored = "", debug = "" // keep track of what is happening // value to use: new if nothing found // otherwise we re-use the existing one, cuz ZOMBIE!! var rndStrX = "" function lookup_cookie(name) { name = name +"=" let decodedCookie = decodeURIComponent(document.cookie) let ca = decodedCookie.split(';') for(let i = 0 ; i < ca.length; i++) { let c = ca[i] while (c.charAt(0) == ' ') { c = c.substring(1) } if (c.indexOf(name) == 0) { return c.substring(name.length, c.length) } } return "" } function check_cookie(name) { if (ckie == true) { // we already checked that cookies work with no errors cookieStored = lookup_cookie(name) if (cookieStored != "") { // we found something debug += "<br>zombie "+ name +" cookie found" dom.ckieP.innerHTML = "<span class='bad'>zombie "+ name +" cookie found:</span> value <span class='bad'>"+ cookieStored +"</span>" } else { // nothing found debug += "<br>zombie "+ name +" cookie: nothing found" } } } function test_cookie() { // cookie support if (navigator.cookieEnabled == true) { debug = "cookies: enabled" dom.ckieE = "enabled" // random let rndStrC = rnd_string("ptest_") let rndStrD = rnd_string("") let d = new Date() d.setTime(d.getTime() + 86400000) // 1 day let expires = "expires="+ d.toUTCString() // set cookie document.cookie = rndStrC +"="+ rndStrD +";"+ expires // look it up let pcookievalue = lookup_cookie(rndStrC) if (pcookievalue != "") { if (pcookievalue == rndStrD) { // value matches debug += "<br>cookies: test successful" ckie = true } else { // value doesn't match: this should never happen? debug += "<br>cookies: test failed: values do not match" } } else { debug += "<br>cookies: test failed" } } else { debug = "cookies: disabled" dom.ckieE = "disabled" } } function set_zombie_cookie(name, value) { // set cookie let d = new Date() d.setTime(d.getTime() + 86400000) // 1 day let expires = "expires="+ d.toUTCString() document.cookie = name +"="+ value +";"+ expires // look it up let pcookievalue = lookup_cookie(name) if (pcookievalue != "") { if (pcookievalue == value) { // value matches debug += "<br>zombie "+ name +" cookie: successfully set" dom.ckieP.innerHTML = "<span class='good'>nothing found:</span> setting new zombie "+ name +" cookie: value <span class='good'>"+ value +"</span>" } else { // value doesn't match: this should never happen? debug += "<br>zombie "+ name +" cookie: test failed: values do not match" } } else { debug += "<br>zombie "+ name +" cookie: test failed" } } function check_storage(key) { if (ls == true) { // we already checked that localStorage works with no errors lsStored = localStorage.getItem(key) debug += "<br>zombie "+ key if (lsStored == null) { // nothing found debug += " key: nothing found" } else { // we found something debug += " key found" dom.ls.innerHTML = "<span class='bad'>zombie "+ key +" key found:</span> value " +"<span class='bad'>"+ lsStored +"</span>" } } } function test_storage() { debug = debug +"<br>---" try { if (typeof(localStorage) != "undefined") { debug += "<br>localStorage: enabled" // localStorage test try { let rndStrE = rnd_string("test_") let rndStrF = rnd_string("") localStorage.setItem(rndStrE, rndStrF) let lsvalue = localStorage.getItem(rndStrE) if (lsvalue == null) { debug += "<br>localStorage: test failed" } else { if (lsvalue == rndStrF) { // values match ls = true debug += "<br>localStorage: test successful" } else { // value doesn't match: this should never happen? debug += "<br>localStorage: test failed: values do not match" } } } catch(e) { debug += "<br>localStorage: test failed: "+ e.name } } else { debug += "<br>localStorage: disabled: undefined" } } catch(e) { debug += "<br>localStorage: disabled: "+ e.name } } function set_zombie_storage(key, value) { try { localStorage.setItem(key, value) let lsvalue = localStorage.getItem(key) if (lsvalue == null) { debug += "<br>zombie "+ key +" key: failed" } else { if (lsvalue == value) { // values match debug += "<br>zombie "+ key +" key: successfully set" dom.ls.innerHTML = "<span class='good'>nothing found:</span> setting new zombie "+ key +" key: value <span class='good'>"+ value +"</span>" } else { // value doesn't match: this should never happen? debug += "<br>zombie "+ key +" key: failed: values do not match" } } } catch(e) { debug += "<br>zombie "+ key +" key: failed: "+ e.name } } function check_idb(name, object, id) { // name: TZP // object: zombie // id: 1 if (idb == true) { // we already checked that idb works with no errors let check = indexedDB.open(name) // create objectStore check.onupgradeneeded = function(event){ let db = event.target.result let store = db.createObjectStore(object, {keyPath: "id"}) } check.onsuccess = function(event) { let db = event.target.result // start transaction let transaction = db.transaction(object, "readwrite") let store = transaction.objectStore(object) let request = store.get(id) // query the data let getStr = store.get(id) getStr.onsuccess = function() { try { idbStored = getStr.result.value // we found something debug += "<br>zombie "+ name +" store found" dom.idb.innerHTML = "<span class='bad'>zombie "+ name +" store found:</span> value " +"<span class='bad'>"+ idbStored +"<span>" } catch (e) { // the value doesn't exist? debug += "<br>zombie "+ name +" store: nothing found" } } // close transaction db.oncomplete = function() {object.close()} } } } function test_idb() { debug = debug +"<br>---" // idb support try { if (!window.indexedDB) { debug += "<br>idb: disabled" } else { debug += "<br>idb: enabled" // idb test try { let dbIDB = indexedDB.open("_testPBMode") dbIDB.onerror = function() { // current pb mode debug += "<br>idb: test failed: onerror" } dbIDB.onsuccess = function() { idb = true let rndStrI = rnd_string("test_") // normal mode try { let openIDB = indexedDB.open(rndStrI) // create objectStore openIDB.onupgradeneeded = function(event){ let dbObject = event.target.result let dbStore = dbObject.createObjectStore("testIDB", {keyPath: "id"}) } // test openIDB.onsuccess = function(event) { let dbObject = event.target.result // start transaction let dbTx = dbObject.transaction("testIDB", "readwrite") let dbStore = dbTx.objectStore("testIDB") // add some data let rndIndex = rnd_number() let rndValue = rnd_string("") dbStore.put({id: rndIndex, value: rndValue}) // query the data let getStr = dbStore.get(rndIndex) getStr.onsuccess = function() { if (getStr.result.value == rndValue) { // values match debug += "<br>idb: test successful" } else { // value doesn't match: this should never happen? debug += "<br>idb: test failed: values didn't match" } } // close transaction dbTx.oncomplete = function() {dbObject.close()} } } catch(e) { debug += "<br>idb: test failed: "+ e.name } } } catch(e) { // blocking cookies or something debug += "<br>idb: failed: .open: "+ e.name } } } catch(e) { debug += "<br>idb: disabled: "+ e.name } } function set_zombie_idb(name, object, zid, zvalue) { let openIDB = indexedDB.open(name) // create objectStore openIDB.onupgradeneeded = function(event){ let dbObject = event.target.result let dbStore = dbObject.createObjectStore(object, {keyPath: "id"}) } // test openIDB.onsuccess = function(event) { let dbObject = event.target.result // start transaction let dbTx = dbObject.transaction(object, "readwrite") let dbStore = dbTx.objectStore(object) // add the data dbStore.put({id: zid, value: zvalue}) // query the data let getStr = dbStore.get(zid) getStr.onsuccess = function() { if (getStr.result.value == zvalue) { // values match debug += "<br>zombie "+ name +" store: successfully set" dom.idb.innerHTML = "<span class='good'>nothing found:</span> setting new zombie "+ name +" store: value <span class='good'>"+ zvalue +"</span>" } else { // value doesn't match: this should never happen? debug += "<br>zombie "+ name +" store: failed: values do not match" } } // close transaction dbTx.oncomplete = function() {dbObject.close()} } } function populate_data() { debug = debug +"<br>---" // lsStored is a pita: can be a null: convert that if (lsStored == null) { lsStored = "" } // unique string for kicks rndStrX = rnd_string("") // any zombies: use that instead // if multiple: they should always be the same: maybe I can check that in the future if (cookieStored !== "") {rndStrX = cookieStored} if (lsStored !== "") {rndStrX = lsStored} if (idbStored !== "") {rndStrX = idbStored} // repopulate if (ckie == true) { if (cookieStored == "") { set_zombie_cookie("TZP", rndStrX) } } if (ls == true) { if (lsStored == "") { set_zombie_storage("TZP", rndStrX) } } if (idb == true) { if (idbStored == "") { set_zombie_idb("TZP", "zombie", "1", rndStrX) } } } function run_sanitize_check() { // clear let items = document.getElementsByClassName("c") for(let i=0; i < items.length; i++) {items[i].innerHTML = "&nbsp"} // not coded yet items = document.getElementsByClassName("faint") for (let i=0; i < items.length; i++) {items[i].textContent = "not coded yet"} // use a delay so user can see things are cleared setTimeout(function() { // reset ckie = false, ls = false, idb = false, cookieStored = "", lsStored = "", idbStored = "", debug = "" // tests test_cookie() check_cookie("TZP") test_storage() check_storage("TZP") test_idb() setTimeout(function() {check_idb("TZP", "zombie", "1")}, 200) // wait for idb tests to finish setTimeout(function() { // ToDo: use promises, put results into an array, output after promise.all, output perf if (ckie == false) {dom.ckieP = zNA} if (ls == false) {dom.ls = zNA} if (idb == false) {dom.idb = zNA} populate_data() // we have to wait for idb again, godamnit setTimeout(function() { dom.debug.innerHTML = debug }, 700) }, 700) }, 170) } run_sanitize_check() </script> </body> </html> ================================================ FILE: tests/screeniframe.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>screen iframe</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 380px;} #tb1 td {padding-right: 10px;} #vwh { height: 100vh; width: 100vw; position: fixed; left: 0; border: 0px; z-index: -6000; } </style> </head> <body> <div id="vwh"></div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html">return to TZP index</a></td></tr> </table> <table id="tb1"> <col width="30%"><col width="70%"> <thead><tr><th colspan="2"> <div class="nav-title">screen iframe <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td></td><td class="s1">top level document</td></tr> <tr><td>screen</td><td class="c mono spaces" id="doc0"></td></tr> <tr><td>available screen</td><td class="c mono spaces" id="doc1"></td></tr> <tr><td>outer</td><td class="c mono spaces" id="doc2"></td></tr> <tr><td>inner</td><td class="c mono spaces" id="doc3"></td></tr> <tr><td>viewport units</td><td class="c mono spaces" id="vunits"></td></tr> <tr><td colspan="2"></td></tr> <tr><td></td><td class="s1">nested iframe | 100vw 100vh</td></tr> <tr><td>screen</td><td class="c mono spaces" id="nest0"></td></tr> <tr><td>available screen</td><td class="c mono spaces" id="nest1"></td></tr> <tr><td>outer</td><td class="c mono spaces" id="nest2"></td></tr> <tr><td>inner</td><td class="c mono spaces" id="nest3"></td></tr> <tr><td colspan="2"></td></tr> <tr><td></td><td class="s1">nested iframe</td></tr> <tr><td>screen</td><td class="c mono spaces" id="nest4"></td></tr> <tr><td>available screen</td><td class="c mono spaces" id="nest5"></td></tr> <tr><td>outer</td><td class="c mono spaces" id="nest6"></td></tr> <tr><td>inner</td><td class="c mono spaces" id="nest7"></td></tr> </table> <br> <script> 'use strict'; function run() { //let t0 = performance.now() try { let vtarget = dom.vwh.getBoundingClientRect() dom.vunits.innerHTML = vtarget.width +' x '+ vtarget.height } catch(e) { dom.wunits = 'error' } const getNestediFrameWindow = () => { const numberOfIframes = window.length const div = document.createElement('div') //div.setAttribute('style', 'display:none') // this line will cause inner to return zero document.body.appendChild(div) const id = 'parent-nest' const ghost = ` style=" height: 100vh; width: 100vw; position: absolute; left: -10000px; visibility: hidden; " ` div.innerHTML = ` <div ${ghost} id="${id}"> <iframe ${ghost}></iframe> </div> ` const el = document.getElementById(id) return { parent: !!el, iframeWindow: window[numberOfIframes], remove: () => el.parentNode.removeChild(el) } } const getNestediFrameWindow2 = () => { const numberOfIframes = window.length const div = document.createElement('div') //div.setAttribute('style', 'display:none') // this line will cause inner to return zero document.body.appendChild(div) const id = 'parent-nest2' const ghost = ` style=" /* height: 100vh; width: 100vw; */ position: absolute; left: -10000px; visibility: hidden; " ` div.innerHTML = ` <div ${ghost} id="${id}"> <iframe ${ghost}></iframe> </div> ` const el = document.getElementById(id) return { parent: !!el, iframeWindow: window[numberOfIframes], remove: () => el.parentNode.removeChild(el) } } dom.doc0 = screen.width +" x "+ screen.height dom.doc1 = screen.availWidth +" x "+ screen.availHeight dom.doc2 = window.outerWidth +" x "+ window.outerHeight dom.doc3 = window.innerWidth +" x "+ window.innerHeight const nest = getNestediFrameWindow() let target = nest.iframeWindow.screen dom.nest0 = target.width +" x "+ target.height dom.nest1 = target.availWidth +" x "+ target.availHeight target = nest.iframeWindow.window dom.nest2 = target.outerWidth +" x "+ target.outerHeight dom.nest3 = target.innerWidth +" x "+ target.innerHeight if (nest.parent) {nest.remove()} const nest2 = getNestediFrameWindow2() target = nest2.iframeWindow.screen dom.nest4 = target.width +" x "+ target.height dom.nest5 = target.availWidth +" x "+ target.availHeight target = nest2.iframeWindow.window dom.nest6 = target.outerWidth +" x "+ target.outerHeight dom.nest7 = target.innerWidth +" x "+ target.innerHeight if (nest2.parent) {nest2.remove()} } run() </script> </body> </html> ================================================ FILE: tests/screenorientation.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>screen orientation</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 380px;} #tb1 td {padding-right: 10px;} @media (-moz-device-orientation:portrait){#cssMoz:after{content:"portrait";}} @media (-moz-device-orientation:landscape){#cssMoz:after{content:"landscape";}} @media (device-aspect-ratio:1/1){#cssDAR:after{content:"square";}} @media (min-device-aspect-ratio:10000/9999){#cssDAR:after{content:"landscape";}} @media (max-device-aspect-ratio:9999/10000){#cssDAR:after{content:"portrait";}} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#screen">return to TZP index</a></td></tr> </table> <table id="tb1"> <col width="50%"><col width="50%"> <thead><tr><th colspan="2"><div class="nav-title">screen orientation</div></th></tr></thead> <tr><td colspan="2" class="intro"><span class="no_color">Testing screen values and actions: <code>screen.orientation</code> change and <code>window</code> resize listeners</span></td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td> <div class="btn-left"><span class="btn1 btn" onClick="run('manual')">[ re-run ]</span> </div>last updated</td><td class='mono' id="last"></td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td>-moz-device-orientation</td><td class='mono' id="-moz-device-orientation"></td></tr> <tr><td>[pseudo] -moz-device-orientation</td><td class='mono' id="-moz-device-orientation_css"></td></tr> <tr><td>device-aspect-ratio</td><td class='mono' id="device-aspect-ratio"></td></tr> <tr><td>[pseudo] device-aspect-ratio</td><td class='mono' id="device-aspect-ratio_css"></td></tr> <tr><td>mozOrientation</td><td class='mono' id="mozOrientation"></td></tr> <tr><td>orientation.angle</td><td class='mono' id="orientation.angle"></td></tr> <tr><td>orientation.type</td><td class='mono' id="orientation.type"></td></tr> <tr><td colspan="2"></td></tr> <tr><td><span class="no_color">hash<span></td><td class='mono' id="hash"></td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td>[css] -moz-device-orientation</td><td class='mono' id="cssMoz"></td></tr> <tr><td>[css] device-aspect-ratio</td><td class='mono' id="cssDAR"></td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><hr></td></tr> <tr><td>[inner] window size</td><td class='mono' id="size"></td></tr> <tr><td>events</td><td class='mono' id="events"></td></tr> </table> <br> <script> 'use strict'; let isLoaded = false let option = {day: '2-digit', month: '2-digit', year: 'numeric', hour12: false, hour: '2-digit', minute: 'numeric', second: 'numeric'} let aEvents = [] function run(trigger) { let list = [ '-moz-device-orientation', 'device-aspect-ratio', 'mozOrientation', 'orientation.angle', 'orientation.type' ] let oData = {} let l = 'landscape', p = 'portrait', q = '(orientation: ', s = 'square', a = 'aspect-ratio' list.forEach(function(item) { let value, cssID try { if ('-moz-device-orientation' == item) { cssID = '#cssMoz' if (window.matchMedia('(-moz-device-orientation:'+ l +')').matches) value = l if (window.matchMedia('(-moz-device-orientation:'+ p +')').matches) value = p } else if ('device-aspect-ratio' == item) { cssID = '#cssDAR' if (window.matchMedia('(device-'+ a +':1/1)').matches) value = s if (window.matchMedia('(min-device-'+ a +':10000/9999)').matches) value = l if (window.matchMedia('(max-device-'+ a +':9999/10000)').matches) value = p } else if ('mozOrientation' == item) { value = screen.mozOrientation } else if ('orientation.angle' == item) { value = screen.orientation.angle } else { value = screen.orientation.type } } catch(e) { value = e +'' } oData[item] = value if (cssID !== undefined) { let cssvalue try { let target = window.getComputedStyle(document.querySelector(cssID), ':after') cssvalue = target.getPropertyValue('content') cssvalue = cssvalue.replace(/['"]+/g, '') } catch(e) { cssvalue = e+'' } oData[item +'_css'] = cssvalue } }) for (const k of Object.keys(oData)) {dom[k].innerHTML = oData[k]} let hash = mini(oData) let notation = hash == 'a1de035c' ? sg +' [RFP desktop]' + sc : '' dom.hash.innerHTML = s1 + hash + sc + notation dom.size.innerHTML = window.innerWidth +' x '+ window.innerHeight let last = (new Date()).toLocaleDateString("en", option) last = last.split(', ')[1] dom.last.innerHTML = last aEvents.push(trigger + ": " + last) try { if (aEvents.length) { //limit to four events if (aEvents.length > 5) {aEvents = aEvents.slice(-5)} dom.events.innerHTML = aEvents.join("<br>") } } catch(e) {} } run('loaded') setTimeout(function(){ screen.orientation.addEventListener('change', function(){run('change')}) window.addEventListener('resize', function(){run('resize')}) }, 100) </script> </body> </html> ================================================ FILE: tests/scroll.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=500"> <title>scrolling</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <!-- custom --> <style> table {width: 480px;} #scrolltarget { position: fixed; top: 0px; left: 0px; height: 1px; width: 1px; overflow: scroll; z-index: -999; } #scrollinner { height: 3px; width: 50px; } </style> </head> <body> <!--<div id="scrolltarget"><div id="scrollinner"></div></div>--> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#devices">return to TZP index</a></td></tr> </table> <table id="tb7"> <col width="30%"><col width="70%"> <thead><tr><th colspan="2"> <div class="nav-title">scrolling <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">minimalist (and faster!) smooth scrolling test using an element based on <a class="blue" target ="_blank" href="https://dlrobertson.com/examples/scrollinto-view-scrollend.html">Dan Robertson's</a> test. <br><br>prefs<ul> <li><code>RFP</code></li> <li><code>general.smoothScroll</code></li> <li><code>ui.prefersReducedMotion</code> (0 = off, 1 = on)</li> </ul> </span> </td></tr> <tr><td colspan="2" class="mono" style="text-align: left; vertical-align: top;"> <span class="btn7 btnfirst" onClick="run(130)">[ run ]</span> | <span>select length <input type="radio" id="len" name="len" value="4"><label>4</label> <input type="radio" id="len" name="len" value="10"><label>10</label> <input type="radio" id="len" name="len" value="50" checked><label>50</label> <input type="radio" id="len" name="len" value="100"><label>100</label> <input type="radio" id="len" name="len" value="600"><label>600</label> </span> <br><br><hr> </td></tr> <tr><td>smooth scroll &nbsp;</td><td><span class="c mono spaces" id="result"></span></td></tr> <tr><td>scroll events &nbsp;</td><td><span class="c mono spaces" id="count"></span></td></tr> <tr><td></td><td><span class="c mono spaces" id="detail"></span></td></tr> </table> <br> <script> 'use strict'; // https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/42070 // https://dlrobertson.com/examples/scrollinto-view-scrollend.html // note: drop document scroll: issues with movement, innerwindow size etc // ToDo: what does it return on android const get_scroll = () => new Promise(resolve => { let t0 = performance.now() let eScrollCount = 0, eEndCount = 0, eEvents = [] function exit() { dom.detail.innerHTML = eEvents.join("<br>") return resolve(eScrollCount) } function onTargetScroll(e) { eScrollCount++ dom.count = eScrollCount + (eScrollCount > 2 ? " (smooth scroll enabled)": "") let x = Math.abs(dom.scrollinner.getBoundingClientRect().x) eEvents.push((eScrollCount+'').padStart(2,' ') +" : "+ (Math.round(performance.now() - t0) +" ms").padStart(6,' ') +" : "+ x ) //dom.detail.innerHTML = eEvents.join("<br>") e.stopPropagation() if (eScrollCount > 2) {exit()} } function onTargetScrollend(e) { eEndCount++ eEvents.push("scrollend "+ eEndCount +": "+ Math.round(performance.now() - t0) +" ms") dom.detail.innerHTML = eEvents.join("<br>") exit() // we only ever expect 1 scrollend } try { // recreate element: this means it's always at 0,0 and no need to run setup try {document.getElementById("scrolltarget").remove()} catch(e) {} const doc = document const id = 'scrolltarget' const div = doc.createElement('div') div.setAttribute('id', id) doc.body.appendChild(div) doc.getElementById(id).innerHTML = "<div id='scrollinner'></div>" // set element width let len = ((document.querySelector('input[name="len"]:checked').value) * 1) // + 1 // don't use len == 3 as this would be 2 scroll events which can periodically also happen without smooth scroll // and if we don't use 3, then we don't need to reset the posiiton (1,1) because (0,0) is good dom.scrollinner.style.width = len +"px" // reset scroll position // without this sometimes we can end up with 1 event (with smooth scroll = false) // we should promise this before we run the test scrolltarget.scrollTo(0, 0) setTimeout(function() { eScrollCount = 0 eEndCount = 0 eEvents = [] // add listeners scrolltarget.addEventListener("scroll", onTargetScroll) scrolltarget.addEventListener("scrollend", onTargetScrollend) scrollinner.scrollIntoView({inline: "end", block: "end", behavior: "smooth"}); }, 20) // ensure we only get 1 scrollend } catch(e) { return resolve(e+"") } }) function run() { dom.result = "" dom.count = "" dom.detail = "" let t0 = performance.now() Promise.all([ get_scroll() ]).then(function(results){ let smoothScroll = results[0] if ("number" === typeof smoothScroll) { smoothScroll = (results[0] > 2 ? "enabled" : "disabled") } dom.result = smoothScroll + " [" + (Math.round(performance.now() - t0) +" ms]") }) } run() </script> </body> </html> ================================================ FILE: tests/supportedlocales.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=800"> <title>supportedlocalesof</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 780px;} .btn, .btnfirst {padding-right: 0px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td></tr> </table> <table id="tb4"> <col width="32%"><col width="68%"> <thead><tr><th colspan="2"> <div class="nav-title">supportedlocalesof <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Return supported locales, including mappings and duplicates, for each given Intl constructor. The list is a base list and supplemented in TZP entropy PoCs, and is not a complete list of all locales.</span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span id="bC" class="btn4 btnfirst" onClick="run('C')">[C]</span> <span id="bDTF" class="btn4 btn" onClick="run('DTF')">[DTF]</span> <span id="bDN" class="btn4 btn" onClick="run('DN')">[DN]</span> <span id="bDF" class="btn4 btn" onClick="run('DF')">[DF]</span> <span id="bLF" class="btn4 btn" onClick="run('LF')">[LF]</span> <span id="bNF" class="btn4 btn" onClick="run('NF')">[NF]</span> <span id="bPR" class="btn4 btn" onClick="run('PR')">[PR]</span> <span id="bRTF" class="btn4 btn" onClick="run('RTF')">[RTF]</span> <span id="bS" class="btn4 btn" onClick="run('S')">[S]</span> <span id="bALL" class="btn4 btn" onClick="run('ALL')">[ALL]</span> <input type="checkbox" id="optExpanded" onChange="run()"> expand <br><br><hr><br> <span id ="results"></span> </td></tr> </table> <br> <script> 'use strict'; var list = [], listExpanded = [], aLegend = [], aLegendExpanded = [], lastMethod, aLocales = [], aLocalesExpanded = [], aSummary = [], aSupported = [], aNotSupported = [] function legend() { // build once let isExpanded = dom.optExpanded.checked if (!isExpanded && aLegend.length == 0 || isExpanded && aLegendExpanded.length == 0) { let listUsed = isExpanded ? listExpanded : list listUsed.sort() for (let i = 0 ; i < listUsed.length; i++) { let str = listUsed[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' if (isExpanded) { aLocalesExpanded.push(code) } else { aLocales.push(code) } let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } if (isExpanded) { aLegendExpanded.push(code.padStart(7) +": "+ name) } else { aLegend.push(code.padStart(7) +": "+ name) } } } let legendUsed = isExpanded ? aLegendExpanded : aLegend // output dom.legend.innerHTML = s4 +"LEGEND ["+ legendUsed.length +"]"+ sc +"<br><br>"+ legendUsed.join("<br>") } function run_main(method, isLoopy) { let t0 = performance.now() let legend = [], data = [], map = [], nocase = [], nocasemap = [], all = [] let spacer = "<br><br>", hashLookup = "", lookupSummary = "" let display = [] let isExpanded = dom.optExpanded.checked aSupported = [] aNotSupported = [] function getPretty(method) { let pretty = "" if (method == "C") {pretty = "collator" } else if (method == "DTF") {pretty = "datetimeformat" } else if (method == "DN") {pretty = "displaynames" } else if (method == "DF") {pretty = "durationformat" } else if (method == "LF") {pretty = "listformat" } else if (method == "NF") {pretty = "numberformat" } else if (method == "PR") {pretty = "pluralrules" } else if (method == "RTF") {pretty = "relativetimeformat" } else if (method == "S") {pretty = "segmenter" } return pretty } try { // hash function getNotation(hash, method) { if (!isFF || isExpanded) return "" // only hash notate if 140+ let notation = "" if (isVer > 139) { if (method == "C") { if (hash == "cdcd1420") {notation = " [FF150+]" // 120 : 1937541: dropped dz, sa, wae } else if (hash == "50c5b9d7") {notation = " [FF147-159]" // 123 - added 'ba' (bashkir) } else if (hash == "6eb2ef9e") {notation = " [FF140-146]" // 122 } else { notation = " "+ zNEW } } else { if (hash == "0376e707") {notation = " [FF147+]" // 245 } else if (hash == "66c554ac") {notation = " [FF140-14]" // 245 } else { notation = " "+ zNEW } } if (notation !== zNEW) notation = s14 + notation + sc } return notation } // get data let listUsed = isExpanded ? listExpanded : list let aLocalesUsed = isExpanded ? aLocalesExpanded : aLocales function getSupported(type, method) { data = [] map = [] all = [] for (let i = 0 ; i < listUsed.length; i++) { // split: code, name let code = listUsed[i].split(",")[0] let name = listUsed[i].split(",")[1] code = code.toLowerCase() // test let test = "" if (method == "C") { test = Intl.Collator.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "DTF") { test = Intl.DateTimeFormat.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "DN") { test = Intl.DisplayNames.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "DF") { test = Intl.DurationFormat.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "LF") { test = Intl.ListFormat.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "NF") { test = Intl.NumberFormat.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "PR") { test = Intl.PluralRules.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "RTF") { test = Intl.RelativeTimeFormat.supportedLocalesOf([code], {localeMatcher: type}) } else if (method == "S") { test = Intl.Segmenter.supportedLocalesOf([code], {localeMatcher: type}) } if (test.length) { let found = test[0].toLowerCase() all.push(found +":"+ code) if (type == "lookup") { nocase.push(found.toLowerCase()) if (code.toLowerCase() !== found.toLowerCase()) { nocasemap.push(code) } } if (code !== found) { map.push(code +" -> "+ found) data.push(code +" -> "+ found) if ('lookup' == type) {aSupported.push(found.toLowerCase())} } else { data.push(found) if ('lookup' == type) {aSupported.push(found.toLowerCase())} } } } // don't sort or remove any dupes let hash = mini(data.join()) // dupes all.sort() let dupes = [], dupesclean = [], tmpCodes = [], tmpCount = 0 let nextItem = "" for (let i = 0 ; i < all.length; i++) { let a = all[i].split(":")[0] // found locale let b = all[i].split(":")[1] // tested code tmpCodes.push(b) if (i < all.length - 1) { nextItem = all[(i+1)].split(":")[0] } else { nextItem = "end" } if (nextItem !== a) { if (tmpCodes.length > 1) { dupes.push(s14 + a + sc + s4 +" ["+ tmpCodes.length +"] "+ sc + tmpCodes.join(", ")) dupesclean.push(a) } tmpCodes = [] tmpCount = 0 } } // not in legend let notlist = [], notlistclean = [], map2 = [] for (let i = 0 ; i < map.length; i++) { let x = map[i].split(" -> ")[0] // tested let y = map[i].split(" -> ")[1] // found if (!aLocalesUsed.includes(y.toLowerCase())) { // case insenstive notlist.push(s16 + y + sc +" ["+ x +"]") notlistclean.push(y) } map2.push(s12 + x + sc + " -> "+ y) if ('lookup' == type) {aSupported.push(x.toLowerCase())} } // color up results if (!isLoopy) { for (let i = 0 ; i < data.length; i++) { let r = data[i] if (map.includes(r)) { let part1 = r.split(" -> ")[0], part2 = r.split(" -> ")[1] if (dupesclean.includes(part2)) { data[i] = s12 + part1 +" -> "+ sc + s14 + part2 + sc } else if (notlistclean.includes(part2)) { data[i] = s12 + part1 +" -> "+ sc + s16 + part2 + sc } else { data[i] = s12 + r + sc } } else { if (dupesclean.includes(r)) { data[i] = s14 + r + sc } } } } // output let str = "" if (type == "lookup") {hashLookup = hash} if (isLoopy) { // build summary if (type == "lookup") { lookupSummary = "<ul><li>" + hashLookup + s4 +" ["+ data.length +"]"+ sc + getNotation(hash, method) +"</li>" if (map2.length) {lookupSummary += "<li>"+ s12 +"MAPPED: "+ sc + map2.join(", ") +"</li>"} if (dupes.length) {lookupSummary += "<li>"+ s14 +"DUPES: "+ sc + dupes.join(", ") +"</li>"} if (notlist.length) {lookupSummary += "<li>"+ s16 +"NOT LISTED: "+ sc + notlist.join(", ") +"</li>"} lookupSummary += "</ul>" } else { str = s4 + getPretty(method).toUpperCase() + sc + (hash == hashLookup ? "" : sb +" [localeMatcher mismatch]"+ sc) str += "<br>" + lookupSummary aSummary.push(str) } } else { str = s4 + type.toUpperCase() + sc let strNotSupported = '' if (type == "lookup") { aNotSupported = aLocalesUsed.filter(x => !aSupported.includes(x)) if (aNotSupported.length) { strNotSupported = spacer + s16 + 'NOT SUPPORTED '+ sc + mini(aNotSupported) + ' ['+ aNotSupported.length + ']' + spacer + "<span class='faint'>" + aNotSupported.join(", ") +"</span>" } str = s4 + getPretty(method).toUpperCase() + sc + spacer + str + spacer str += sg + hash + sc + s4 +" ["+ data.length +"]"+ sc + getNotation(hash, method) // nocase used to color legend items nocase.sort() nocase = nocase.filter(function(item, position) {return nocase.indexOf(item) === position}) nocasemap.sort() nocasemap = nocasemap.filter(function(item, position) {return nocasemap.indexOf(item) === position}) } else { str = spacer + "<hr>" + "<br>" + str + spacer str += hash + s4 +" ["+ data.length +"]"+ sc + (hash == hashLookup ? sg : sb) +" [match]"+ sc } str += spacer + "<span class='faint'>" + data.join(", ") +"</span>" if (map2.length) {str += spacer + s12 +"MAPPED"+ sc + spacer + map2.join("<br>")} if (dupes.length) {str += spacer + s14 +"DUPES"+ sc + spacer + dupes.join("<br>")} if (notlist.length) {str += spacer + s16 +"NOT LISTED"+ sc + spacer + notlist.join("<br>")} str += strNotSupported display.push(str) } } getSupported("lookup", method) getSupported("best fit", method) if (!isLoopy) { dom.results.innerHTML = display.join("<br>") // build + color legend for (let i = 0 ; i < listUsed.length; i++) { // split: code, name let str = listUsed[i].toLowerCase() let code = str.split(",")[0].trim() let name = (undefined !== str.split(",")[1]) ? str.split(",")[1].trim() : '' if (nocase.includes(code.toLowerCase())) { let isSplit = (name.includes("(") && (name.length + code.length) > 32) if (name.includes("(")) { let name0 = name.split("(")[0].trim() let name1 = name.substring( name.indexOf("(") + 1, name.lastIndexOf(")") ) name1 = s99 +"("+ name1 + ")"+ sc if (isSplit) { name = name0 +"<br>"+ " ".repeat(4) + name1 } else { name = name0 +" "+ name1 } } legend.push(sg + code.padStart(7) + sc +": "+ name) } else { if (nocasemap.includes(code)) { legend.push(s12 + code.padStart(7) +": "+ name + sc) } else { legend.push(code.padStart(7) +": "+ name) } } } // display legend dom.legend.innerHTML = s4 +"LEGEND ["+ legend.length +"]"+ sc +"<br><br>"+ legend.join("<br>") // perf dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } } catch(e) { // catch unsupported let msg = e.message if (isFF) {msg = msg.replace("can't access property \"supportedLocalesOf\", ", "")} // trim *error_fix if (isLoopy) { let x = s4 + getPretty(method).toUpperCase() + sc + "<ul><li>"+ e.name +": "+ msg +"</li></ul>" aSummary.push(x) } else { dom.results.innerHTML = "<br>"+ s4 + e.name +": "+ sc + msg } } } function run(method) { //reset legend() dom.results = "" dom.perf = "" let delay = 250 if (undefined == method) {method = lastMethod; delay = 0} else {setBtn(method)} lastMethod = method // delay so users see change and allow paint setTimeout(function() { if (method == "ALL") { run_all() } else { run_main(method, false) } }, delay) } function run_all() { let t0 = performance.now() aSummary = [] run_main("C", true) run_main("DTF", true) run_main("DN", true) run_main("DF", true) run_main("LF", true) run_main("NF", true) run_main("PR", true) run_main("RTF", true) run_main("S", true) dom.results.innerHTML = aSummary.join("<br>") let t1 = performance.now() dom.perf.innerHTML = Math.round(t1-t0) +"ms" } function setBtn(method) { if (undefined == method) {return} // reset btns let items = document.getElementsByClassName("btn8") for (let i=0; i < items.length; i++) { items[i].classList.add("btn4") items[i].classList.remove("btn8") } // set btn let el = document.getElementById("b"+ method) el.classList.add("btn8") el.classList.remove("btn4") } legend() Promise.all([ get_globals() ]).then(function(){ get_isVer() list = gLocales let aListExtra = [ /* 'cnr,montenegrin', // maps to sr-me 'gom,goan', // maps to kok 'prp,parsi', // maps to gu 'prs,dari', // maps to fa-af 'sh,serbo-croatian', // maps to sr-latn 'swc,congo', // maps to sw-cd 'tl,tagalog', // maps to fil 'tw,twi', // maps to ak except in collator */ ] list = list.concat(aListExtra) list = list.filter(function(item, position) {return list.indexOf(item) === position}) listExpanded = gLocales = gLocales.concat(gLocalesExpand) listExpanded = listExpanded.filter(function(item, position) {return listExpanded.indexOf(item) === position}) run("ALL") }) </script> </body> </html> ================================================ FILE: tests/supportedvalues.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>supportedvaluesof</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 380px; max-width: 480px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td></tr> </table> <table id="tb4"> <col width="40%"><col width="60%"> <thead><tr><th colspan="2"> <div class="nav-title">supportedvaluesof <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">Return supported values for each parameter</span> </td></tr> <tr><td colspan="2"><hr><br></td></tr> <tr><td class="padr">hash</td><td class="mono spaces" id="hashAll"></td></tr> <tr><td class="padr">calendar</td><td class="mono spaces" id="calendar"></td></tr> <tr><td class="padr">collation</td><td class="mono spaces" id="collation"></td></tr> <tr><td class="padr">currency</td><td class="mono spaces" id="currency"></td></tr> <tr><td class="padr">numberingSystem</td><td class="mono spaces" id="numberingSystem"></td></tr> <tr><td class="padr">timeZone</td><td class="mono spaces" id="timeZone"></td></tr> <tr><td class="padr">unit</td><td class="mono spaces" id="unit"></td></tr> <tr><td colspan="2"><br><hr></td></tr> <tr><td colspan="2" style="text-align: left; color: var(--test0);" class="mono spaces" id="details">click counts to list their results here</td></tr> </table> <br> <script> 'use strict'; let oData = {} function display(item) { let delim = (item == "timeZone" ? "<br>" : ", ") dom.details.innerHTML = s4 + item + sc +"<br><span class='faint'>[<br><span class='indent'>" + oData[item].join(delim) + "</span><br>]</span>" } function run() { let t0 = performance.now() // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf try { let res = [] let list = ["calendar","collation","currency","numberingSystem","timeZone","unit"] list.forEach(function(item) { let array = Intl.supportedValuesOf(item) let el = document.getElementById(item) let hash = mini(array) el.innerHTML = hash + " <span class='btn4 btnc' onclick='display(`" + item + "`)'>[" + array.length +"]</span>" res.push(item +": "+ hash) oData[item] = array }) // overall hash let hash = mini(res), code = "" if (isFF) { // notate new if 140+ if (isVer > 139) { if (hash == "1a35c685") {code = s14 +" [FF150+]"+ sc // collation: -searchjl } else if (hash == "3c1a144a") {code = s14 +" [FF147-149]"+ sc /* calendar: -islamic -islamic-rgsa numberingSystem: +tols */ } else if (hash == "db8d9901") {code = s14 +" [FF140-146]"+ sc /* timezone: +America/Coyhaique */ } else {code = ' '+ zNEW } } } dom.hashAll.innerHTML = hash + code dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" } catch (e) { dom.details = "" dom.detailslabel = e.name dom.details = e.message } } Promise.all([ get_globals() ]).then(function(){ get_isVer() run() }) </script> </body> </html> ================================================ FILE: tests/testgeneric.js ================================================ 'use strict'; dom = getUniqueElements(); /*** GENERIC ***/ const newFn = x => typeof x != 'string' ? x : new Function(x)() function nowFn() { try {return performance.now() } catch(e) {return} } function rnd_string() {return Math.random().toString(36).substring(2, 15)} function rnd_number() {return Math.floor((Math.random() * (99999-10000))+10000)} function count_decimals(value) {if(Math.floor(value) === value) return 0;return value.toString().split(".")[1].length || 0} function removeElementFn(id) {try {dom[id].remove()} catch(e) {}} function cleanFn(item, skipArray = false) { // strings, tidy undefined, empty strings if (typeof item === "number" || typeof item === "bigint") { return item } else if (item == zU) {item = zUQ } else if (item == "true" || item == "false" || item == "null") {item = "\"" + item + "\"" } else if (!skipArray && Array.isArray(item)) { item = !item.length ? "empty array" : "array" } else if (item === undefined || item === true || item === false || item === null) {item += "" } else if (!skipArray && item == "") {item = "empty string" } else if (typeof item === "string") { if (!Number.isNaN(item*1)) {item = "\"" + item + "\""} } return item } function typeFn(item, isSimple = false) { // return a more detailed result let type = typeof item if ("number" === type) { if (Number.isNaN(item)) {type = "NaN"} else if (Infinity === item) {type = 'Infinity'} } else if ("string" === type) { if (!isSimple) { if ("" === item) {type = "empty string"} else if ("" === item.trim()) {type = "whitespace"} } } else if ("object" === type) { if (null === item) {type = "null" } else if (Array.isArray(item)) { type = "array" if (!isSimple) {type = !item.length ? "empty array" : "array"} } else { if (!isSimple) { try {if (0 === Object.keys(item).length) {type = "empty object"}} catch(e) {} } } } // do nothing: undefined, bigint, boolean, function return type +'' } function getUniqueElements() { const dom = document.getElementsByTagName('*') return new Proxy(dom, { get: function(obj, prop) {return obj[prop]}, set: function(obj, prop, val) {obj[prop].textContent = `${val}`; return true} }) } function buildButton(colorCode, arrayName, displayText, functionName, btnType) { if (functionName == undefined) {functionName = "showDetail"} if (btnType == undefined) {btnType = "btnc"} let part1 = arrayName.split(",")[0] let part2 = arrayName.split(",")[1] // pass a second parameter (boolean) e.g. domrectspoof non-FF tests part2 = part2 == undefined ? "" : part2.trim() if (part2 == "true" || part2 == "false") {part2 = ", "+ part2} else {part2 = ""} return " <span class='btn"+ colorCode +" "+ btnType +"' onClick='" + functionName +"(`"+ part1 +"`"+ part2 + ")'>["+ displayText +"]</span>" } /*** JSON ***/ function json_highlight(json, maxWidth = 65) { if (typeof json != 'string') { json = json_stringify(json, {maxLength: maxWidth}); } json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { var cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; // color undefined (aka "typeof undefined") //if (match == "\"typeof undefined\"") {cls = 'null';} } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return '<span class="'+ cls +'">'+ match +'</span>'; }) } function json_stringify(passedObj, options = {maxLength: 65}) { /* https://github.com/lydell/json-stringify-pretty-compact */ const stringOrChar = /("(?:[^\\"]|\\.)*")|[:,]/g; const indent = JSON.stringify( [1], undefined, options.indent === undefined ? 2 : options.indent ).slice(2, -3); const maxLength = indent === "" ? Infinity : options.maxLength === undefined ? 65 // was 80 : options.maxLength; let { replacer } = options; return (function _stringify(obj, currentIndent, reserved) { if (obj && typeof obj.toJSON === "function") { obj = obj.toJSON(); } // display undefined under an alias so we always have the right number of values // this is just a display, it does not alter the fingerprint data //if (obj === undefined) {obj = "typeof undefined"} const string = JSON.stringify(obj, replacer); if (string === undefined) { return string; } const length = maxLength - currentIndent.length - reserved; if (string.length <= length) { const prettified = string.replace( stringOrChar, (match, stringLiteral) => { return stringLiteral || `${match} `; } ); if (prettified.length <= length) { return prettified; } } if (replacer != null) { obj = JSON.parse(string); replacer = undefined; } if (typeof obj === "object" && obj !== null) { const nextIndent = currentIndent + indent; const items = []; let index = 0; let start; let end; if (Array.isArray(obj)) { start = "["; end = "]"; const { length } = obj; for (; index < length; index++) { items.push( _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) || "null" ); } } else { start = "{"; end = "}"; const keys = Object.keys(obj); const { length } = keys; for (; index < length; index++) { const key = keys[index]; const keyPart = `${JSON.stringify(key)}: `; const value = _stringify( obj[key], nextIndent, keyPart.length + (index === length - 1 ? 0 : 1) ); if (value !== undefined) { items.push(keyPart + value); } } } if (items.length > 0) { return [start, indent + items.join(`,\n${nextIndent}`), end].join( `\n${currentIndent}` ); } } return string; })(passedObj, "", 0); } /*** HASH ***/ function mini(str) { // https://stackoverflow.com/a/22429679 const json = `${JSON.stringify(str)}` let i, len, hash = 0x811c9dc5 for (i = 0, len = json.length; i < len; i++) { hash = Math.imul(31, hash) + json.charCodeAt(i) | 0 } return ('0000000' + (hash >>> 0).toString(16)).slice(-8) } function sha1(str1){ for (var blockstart=0, i = 0, W = [], H = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0], A, B, C, D, F, G, word_array = [], temp2, s = unescape(encodeURI(str1)), str_len = s.length; i<=str_len;){ word_array[i>>2] |= (s.charCodeAt(i)||128)<<(8*(3-i++%4)); } word_array[temp2 = ((str_len+8)>>6<<4)+15] = str_len<<3; for (; blockstart <= temp2; blockstart += 16) { A = H,i=0; for (; i < 80; A = [[ (G = ((s=A[0])<<5|s>>>27) + A[4] + (W[i] = (i<16) ? ~~word_array[blockstart + i] : G<<1|G>>>31) + 1518500249) + ((B=A[1]) & (C=A[2]) | ~B & (D=A[3])), F = G + (B ^ C ^ D) + 341275144, G + (B & C | B & D | C & D) + 882459459, F + 1535694389 ][0|i++/20] | 0, s, B<<30|B>>>2, C, D] ) { G = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; } for(i=5;i;) H[--i] = H[i] + A[i] | 0; } for(str1='';i<40;)str1 += (H[i>>3] >> (7-i++%8)*4 & 15).toString(16); return str1 } /*** GLOBAL VARS ***/ const get_globals = () => new Promise(resolve => { // immutables: do once but promise from each test page if used let tstart = nowFn() // we use > not >= which means 50% or more to break an engine check // e.g always use odd // 9 all same: to get under/over 4.5 you would need to lie about 5/9 = 56% // e.g. even // 8 true: to get 4-or-lower you would need to lie about 4/8 // 8 false: to get over 4 you would need to lie about 5/8 let oEngines = { "blink": [ "number" === typeof TEMPORARY, "number" === typeof PERSISTENT, "object" === typeof onappinstalled, "object" === typeof onbeforeinstallprompt, //"object" === typeof onpointerrawupdate, //"object" === typeof onsearch, //"boolean" === typeof originAgentCluster, //"object" === typeof trustedTypes, "function" === typeof webkitResolveLocalFileSystemURL, ], "webkit": [ "object" === typeof browser, //"function" === typeof getMatchedCSSRules, "object" === typeof safari, //"function" === typeof showModalDialog, "function" === typeof webkitConvertPointFromNodeToPage, "function" === typeof webkitCancelRequestAnimationFrame, "object" === typeof webkitIndexedDB, ], "gecko": [ "function" === typeof dump, "boolean" === typeof fullScreen, "number" === typeof mozInnerScreenX, "function" === typeof scrollByLines, "number" === typeof scrollMaxY, "function" === typeof setResizable, //"function" === typeof sizeToContent, // removed nightly FF117+ 1832733 / 1600400 "function" === typeof updateCommands, ], "edgeHTML": [ "function" === typeof clearImmediate, "function" === typeof msWriteProfilerMark, "object" === typeof oncompassneedscalibration, "object" === typeof onmsgesturechange, "object" === typeof onmsinertiastart, "object" === typeof onreadystatechange, //"object" === typeof onvrdisplayfocus, "function" === typeof setImmediate, ] } // array engine matches, so subsequent results doesn't override prev let aEngine = [] for (const engine of Object.keys(oEngines).sort()) { let sumE = oEngines[engine].reduce((prev, current) => prev + current, 0) if (sumE > (oEngines[engine].length/2)) {aEngine.push(engine)} } if (aEngine.length == 1) {isEngine = aEngine[0]} // valid one result // perf let tend = nowFn() // re-tidy vars if (isEngine == "gecko") { isFF = true // check for PM28+ : fails 53 if ("function" !== typeof CSSMozDocumentRule) { isEngine = "goanna" } } // build a pretty display let displayAll = [] for (const engine of Object.keys(oEngines).sort()) { let displayE = [] oEngines[engine].forEach(function(check) { displayE.push(check ? green_tick : red_cross) }) displayAll.push(displayE.join("")) } isEnginePretty = Math.round(tend-tstart) +" ms |" + displayAll.join(" |") + " | " + (isEngine == "" ? "UNKNOWN" : isEngine.toUpperCase()) // isFF: gecko 10 more tests tstart = nowFn() try { let list = [ [DataTransfer, "DataTransfer", "mozSourceNode"], [Document, "Document", "mozFullScreen"], [HTMLCanvasElement, "HTMLCanvasElement", "mozPrintCallback"], [HTMLElement, "HTMLElement", "onmozfullscreenerror"], //[HTMLInputElement, "HTMLInputElement", "mozIsTextField"], // removed FF142 [HTMLVideoElement, "HTMLVideoElement", "mozDecodedFrames"], [IDBIndex, "IDBIndex", "mozGetAllKeys"], [IDBObjectStore, "IDBObjectStore", "mozGetAll"], [Screen, "Screen", "mozOrientation"], [SVGElement, "SVGElement", "onmozfullscreenchange"] ] let obj, aNo = [] list.forEach(function(array) { obj = array[0] if ("function" === typeof obj && ("object" === typeof Object.getOwnPropertyDescriptor(obj.prototype, array[2])) ) { } else { aNo.push(array[1]) } }) let tend = nowFn() let found = (list.length - aNo.length) if (found > 5) { isFF = true isEngine = "gecko" // check for PM28+ : fails 53 if ("function" !== typeof CSSMozDocumentRule) { isEngine = "goanna" } } // build a pretty display let display = [] list.forEach(function(array) { let check = aNo.includes(array[1]) ? red_cross : green_tick display.push(check) }) isFFvalid = true let strYou = " | are you " + (isEngine == "goanna" ? "goanna" : "gecko") + "? " + (isFF ? sg.trim() + "YES" : sb.trim() + "NO") + sc isFFpretty = Math.round(tend-tstart) +" ms |"+ display.join("") + strYou } catch(e) { isFFvalid = false isFFpretty = sb.trim() + e.name +": "+ sc + e.message } return resolve() }) function get_is95() { // requires a dom element return new Promise(resolve => { if (!isFF) { return resolve() } // pre-compute slow 95 test if ("function" === typeof self.structuredClone && "function" !== typeof crypto.randomUUID) { // ^ do if 94+ but not 95+ fast path try { if ("sc" !== Intl.PluralRules.supportedLocalesOf("sc").join()) { // but not if 96+ let ratio = dom.test95a.offsetWidth/dom.test95b.offsetWidth is95 = (ratio > 0.4 && ratio < 0.6) } } catch(e) { console.debug(e.name, e.message) } } return resolve() }) } const get_isVer = () => new Promise(resolve => { // NOTE: requires dom for 95 and 76, and a promised is95 if (!isFF) {return resolve()} function output(verNo) { isVer = verNo return resolve() } // avoid false returns with forks if ("function" !== typeof CSSMozDocumentRule) { // palemoon/basilisk: fails 53 output(52) return } else if ("function" === typeof AbortSignal && "undefined" !== typeof HTMLAppletElement) { // waterfox classic: 57 pass, 56 fail // but waterfox (v78) also fails 56, so test that as well if (!window.Document.prototype.hasOwnProperty("replaceChildren")) { output(52) return } } output(cascade()) function cascade() { isVerMax = 152 // old-timey check: avoid false postives if (CanvasRenderingContext2D.prototype.hasOwnProperty('letterSpacing')) { try {if (SVGTextPathElement.prototype.hasOwnProperty('side')) return 152} catch(e) {} // 2034371 if (CSSContainerRule.prototype.hasOwnProperty('conditions')) return 151 // 2022827 if ('object' == typeof visualViewport.onscrollend) return 150 // 1801658 try {Temporal.PlainDate.from({calendar:'gregory', monthCode:'M12', month:13, year:2019, day:1})} catch(e) {if ('RangeError' == e.name) return 149} // 2009792 // 148: fast-path: pref dom.location.ancestorOrigins.enabled (default true) try {if (undefined !== location.ancestorOrigins) return 148} catch(e) {} // 1085214 try {let test148 = new Temporal.Duration(0).total({unit:'years', relativeTo:'-271821-04-19'}); return 148} catch(e) {} // 2004851 try {if (Intl.supportedValuesOf('numberingSystem').includes('tols')) return 147} catch(e) {} // 2000225 ? try {throw new DOMException('a', 'b')} catch(e) {if (0 !== e.columnNumber) return 146} // 1997216 if (undefined !== (new ToggleEvent('toggle', null)).source) return 145 // 1968987 if (undefined == window.CSS2Properties) return 144 // 144: 1919582 // 143: fast-path: pref: layout.css.moz-appearance.webidl.enabled: default false 143+ if (!CSS2Properties.prototype.hasOwnProperty('-moz-appearance')) return 143 // 1977489 // 142 try { let segmenter = new Intl.Segmenter('en', {granularity:'word'}) let test142 = Array.from(segmenter.segment('a:b')).map(({ segment }) => segment) if (3 == test142.length) return 142 // 1960300 } catch(e) {} // 141: fast-path: requires temporal default enabled FF139+ javascript.options.experimental.temporal try {if (undefined == Temporal.PlainDate.from('2029-12-31[u-ca=gregory]').weekOfYear) return 141} catch(e) {} // 1950162 // 141: fast-path: dom.intersection_observer.scroll_margin.enabled (default true) try {if (window["IntersectionObserver"].prototype.hasOwnProperty('scrollMargin')) return 141} catch(e) {} // 1860030 // 140: fast-path: pref: dom.event.pointer.rawupdate.enabled : default true 140+ try {if ("object" === typeof onpointerrawupdate) return 140} catch(e) {} // 1550462 // 140: if < 141 there is only one paint entry "PerformancePaintTiming" try {if (undefined !== performance.getEntriesByType("paint")[0].presentationTime) return 140} catch(e) {} // 1963464 // 139 try {if (HTMLDialogElement.prototype.hasOwnProperty('requestClose')) return 139} catch(e) {} // 1960556 // 138: fast-path: requires webrtc e.g. media.peerconnection.enabled | --disable-webrtc try {if (RTCCertificate.prototype.hasOwnProperty('getFingerprints')) return 138} catch(e) {} // 1525241 // 138: fast-path: dom.origin_agent_cluster.enabled if ('boolean' == typeof originAgentCluster) return 138 // 1665474 // 138: must be FF134 or higher try { if (HTMLScriptElement.prototype.hasOwnProperty('textContent')) { // FF135+ let test138 = Intl.NumberFormat('yo-bj', {style: 'unit', unit: 'year', unitDisplay: 'narrow'}).format(1) if ('606d1046' == mini(test138)) return 138 // 1954425 } } catch(e) {} // 137 fast-path: javascript.options.experimental.math_sumprecise if ('function' == typeof Math.sumPrecise) return 137 // 1943120 try { // fastpath: FF132+: javascript.options.experimental.regexp_modifiers if ((new RegExp("(?i:[A-Z]{4})")).test('abcd')) return 136 // 1939533 } catch(e) {} if (HTMLScriptElement.prototype.hasOwnProperty('textContent')) return 135 // 1905706 try { if ('lij' == Intl.PluralRules.supportedLocalesOf('lij').join()) return 134 // 1927706 } catch(e) {} try { let parser = (new DOMParser).parseFromString("<select><option name=''></option></select>", 'text/html') if (null === parser.body.firstChild.namedItem('')) return 133 // 1837773 } catch(e) {} try { const re = new RegExp('(?:)', 'gv'); let test132 = RegExp.prototype[Symbol.matchAll].call(re, '𠮷') for (let i=0; i < 3; i++) {if (true == test132.next().done) return 132} // 1899413 } catch(e) {} try { let test131 = new Intl.DateTimeFormat('zh', {calendar: 'chinese', dateStyle: 'medium'}).format(new Date(2033, 9, 1)) if ('2033' == test131.slice(0,4)) return 131 // 1900196 } catch(e) {} try {new RegExp('[\\00]','u')} catch(e) {if (e+'' == 'SyntaxError: invalid decimal escape in regular expression') return 130} // 1907236 if (CSS2Properties.prototype.hasOwnProperty('WebkitFontFeatureSettings')) return 129 // 1595620 try {let test128 = (new Blob()).bytes(); return 128} catch(e) {} // 1896509 try {if ((new Date('15Jan0024')).getYear() > 0) return 127} catch(e) {} // 1894248 if ('function' === typeof URL.parse) {return 126} try {if ('Invalid Date' == new Date('Sep 26 Thurs 1995 10:00')) return 125} catch(e) {} // 1872793 let el = document.documentElement if (!CSS2Properties.prototype.hasOwnProperty('MozUserFocus')) { try { el.style.zIndex = 'calc(1 / max(-0, 0))' let test = getComputedStyle(el).zIndex el.style.zIndex = 'auto' if (test > 0) {return 124} // 1867569 } catch(e) {} return 123 // 1871745 } if ('function' === typeof Promise.withResolvers) { try { el.style.zIndex = 'calc(1 / abs(-0))' let test = getComputedStyle(el).zIndex el.style.zIndex = 'auto' if (test > 0) {return 122} // 1867558 } catch(e) {} return 121 // 1845586 } if (window.hasOwnProperty('UserActivation')) return 120 // 1791079 try {location.href = 'http://a>b/'} catch(e) {if (e.name === 'SyntaxError') return 119} // 1817591 if (CSS2Properties.prototype.hasOwnProperty('fontSynthesisPosition')) return 118 // 1849010 if (CanvasRenderingContext2D.prototype.hasOwnProperty('fontStretch')) return 117 // 1842467 if (CanvasRenderingContext2D.prototype.hasOwnProperty('textRendering')) return 116 // 1839614 return 115 // 1778909 } // 114 or lower if ("function" === typeof CSS2Properties && CSS2Properties.prototype.hasOwnProperty("WebkitTextSecurity")) return 114 // 1826629 if (CanvasRenderingContext2D.prototype.hasOwnProperty("reset")) return 113 // 1709347 if (CanvasRenderingContext2D.prototype.hasOwnProperty("roundRect")) return 112 // 1756175 if (HTMLElement.prototype.hasOwnProperty("translate")) return 111 // 1418449 if ("object" === typeof ondeviceorientationabsolute) return 110 // 1689631 if (CSSKeyframesRule.prototype.hasOwnProperty("length")) return 109 // 1789776 if ("undefined" === typeof onloadend) return 108 // 1574487 if (!SVGSVGElement.prototype.hasOwnProperty("useCurrentView")) return 107 // 1174097 if (Element.prototype.hasOwnProperty("checkVisibility")) return 106 // 1777293 try {structuredClone((() => {}))} catch(e) {if (e.message.length == 36) return 105} // 830716 if (undefined !== window.SVGStyleElement && SVGStyleElement.prototype.hasOwnProperty("disabled")) return 104 // 1712623 if (undefined === new ErrorEvent("error").error) return 103 // 1772494 if (CanvasRenderingContext2D.prototype.hasOwnProperty("direction")) { if (Array(1).includes()) return 102 // 1767541: regression FF99 return 101 // 1728999 } if ("function" === typeof AbortSignal && "function" === typeof AbortSignal.timeout) return 100 // 1753309 try {newFn("class A { #x; h(o) { return !#x in o; }}")} catch(e) {if (e.message.length == 72) return 99} // 1711715 + 1756204 if (HTMLElement.prototype.hasOwnProperty("outerText")) return 98 // 1709790 if ("function" === typeof AbortSignal && "function" === typeof AbortSignal.prototype.throwIfAborted) return 97 // 1745372 if ("undefined" === typeof Object.toSource && "sc" === Intl.PluralRules.supportedLocalesOf("sc").join()) return 96 // 1738422 // ^ legacy perf: toSource (74+): FF68- very slow if ("function" === typeof crypto.randomUUID) return 95 // 1723674: fast path pref if (is95) return 95 // 1674204 // ^ pre-computed if ("function" === typeof self.structuredClone) return 94 // 1722576 if ("function" === typeof self.reportError) return 93 // 1722448 if ("function" === typeof Object.hasOwn) return 92 // 1721149 if ("object" === typeof window.clientInformation) return 91 // 1717072 fast path pref try {if ("sa" === Intl.Collator.supportedLocalesOf("sa").join()) return 91} catch(e) {} // 1714933 if ("function" === typeof Array.prototype.at) return 90 // 1681371 if ("function" === typeof CountQueuingStrategy && ! new CountQueuingStrategy({highWaterMark: 1}).hasOwnProperty("highWaterMark")) return 89 // 1684316 // ^ legacy check FF64- CountQueuingStrategy if (":" === document.createElement("a").protocol) return 88 // 1497557 if (undefined === console.length) return 87 // 1688335 if ("function" === typeof Intl.DisplayNames) return 86 // 1654116 try {Object.getOwnPropertyDescriptor(RegExp.prototype, "global").get.call("/a") } catch(e) {if (e.message.length == 66) {return 85}} // 1675240 // ^ replace ? if ("function" === typeof PerformancePaintTiming) return 84 // 1518999 if ("undefined" === typeof Object.toSource && !window.HTMLIFrameElement.prototype.hasOwnProperty("allowPaymentRequest")) return 83 // 1665252 try {if (1595289600000 === Date.parse('21 Jul 20 00:00:00 GMT')) {return 82}} catch(e) {} // 1655947 // ^ ext fuckery: cydec if (new File(["x"], "a/b").name == "a/b") return 81 // 1650607 if ("function" === typeof CSS2Properties && CSS2Properties.prototype.hasOwnProperty("appearance")) return 80 // 1620467 if ("function" === typeof Promise.any) return 79 // 1599769 shipped if (window.Document.prototype.hasOwnProperty("replaceChildren")) return 78 // 1626015 if (undefined !== window.IDBCursor && window.IDBCursor.prototype.hasOwnProperty("request")) return 77 // 1536540 if ("undefined" === typeof Object.toSource && !test76.validity.rangeOverflow) return 76 // 1608010 if ("function" === typeof Intl.Locale) return 75 // 1613713 if ("undefined" === typeof Object.toSource) return 74 // 1565170 if (!VideoPlaybackQuality.prototype.hasOwnProperty("corruptedVideoFrames")) return 73 // 1602163 if ("boolean" === typeof self.crossOriginIsolated) return 72 // 1591892 if ("function" === typeof Promise.allSettled) return 71 // 1549176 if ("function" === typeof Intl.RelativeTimeFormat && "function" === typeof Intl.RelativeTimeFormat.prototype.formatToParts) return 70 // 1473229 // ^ legacy check: FF64- Intl.RelativeTimeFormat // ^ extension fuckery: formatToParts try {newFn("let t = 1_050"); return 70} catch(e) {} // 1435818 if ("function" === typeof Blob.prototype.text) return 69 // 1557121 if (!HTMLObjectElement.prototype.hasOwnProperty("typeMustMatch")) return 68 // 1548773 if ("function" === typeof String.prototype.matchAll) return 67 // 1531830 if ("function" === typeof HTMLSlotElement && "function" === typeof HTMLSlotElement.prototype.assignedElements) return 66 // 1425685 // ^ legacy check: FF60- HTMLSlotElement if (1 === DataView.length) return 65 // 1334813 if ("number" === typeof window.screenTop) return 64 // 1498860 if ("desc" === Symbol('desc').description) return 63 // 1472170 if ("function" === typeof console.timeLog) return 62 // 1458466 if ("object" === typeof CSS) return 61 // 1455805 if (undefined !== window.Animation && "function" === typeof Animation.prototype.updatePlaybackRate) return 60 // 1436659 if (!HTMLMediaElement.prototype.hasOwnProperty("mozAutoplayEnabled")) return 59 // 1336400 if ("function" === typeof Intl.PluralRules) return 58 // 1403318 if ("function" === typeof AbortSignal) return 57 // 1378342 if ("undefined" === typeof HTMLAppletElement) return 56 // 1279218 if ("undefined" === typeof console.timeline) return 55 // 1351795 if (URL.prototype.hasOwnProperty("toJSON")) return 54 // 1337702 if ("function" === typeof CSSMozDocumentRule) return 53 return 52 } }) /** GENERAL CLICK FUNCTIONS **/ function buildnav() { // add prev/next nav links in all the intl tests so it's easy to loop thru them let aTests = [ // filenames: in order as per listed, not necerssarily alphabetical 'collation', 'dtfcomponents','dtfdatetimestyle','dtfdayperiod','dtflistformat','dtfrelated','dtftimezonename', 'dncalendar','dncurrency','dndatetime','dnlanguage','dnregion','dnscript', 'duration', 'nfcompact','nfcurrency','nfformattoparts','nfnotation','nfsign','nfunit', 'pr','prrange','rtf','resolvedoptions','segmenter', ] let oTests = { // just hardcode the prettyy names 'dncalendar': 'dn: calendar', 'dncurrency': 'dn: currency', 'dndatetime': 'dn: datetimefield', 'dnlanguage': 'dn: language', 'dnregion': 'dn: region', 'dnscript': 'dn: script', 'dtfcomponents': 'dtf: components', 'dtfdatetimestyle': 'dtf: date-&-timestyle', 'dtfdayperiod': 'dtf: dayperiod', 'dtflistformat': 'dtf: listformat', 'dtfrelated': 'dtf: relatedyear', 'dtftimezonename': 'dtf: timezonename', 'duration': 'durationformat', 'nfcompact': 'nf: compact', 'nfcurrency': 'nf: currency', 'nfformattoparts': 'nf: ftp', 'nfnotation': 'nf: notation', 'nfsign': 'nf: sign', 'nfunit': 'nf: unit', 'pr': 'pr: select', 'prrange': 'pr: selectrange', 'rtf': 'relativetimeformat', } // if not file skip segementer if (!isFile) {aTests = aTests.filter(x => !['segmenter'].includes(x)) } // get filename let loc = location +'' let file = loc.slice(loc.lastIndexOf('/') + 1, loc.length - 5) // index let current = aTests.indexOf(file), previous, next, max = (aTests.length - 1) if (current == 0) {previous = max} else {previous = current - 1} if (current == max) {next = 0} else {next = current + 1} // keys/filenames previous = aTests[previous] next = aTests[next] // pretty names let pName = undefined !== oTests[previous] ? oTests[previous] : previous let nName = undefined !== oTests[next] ? oTests[next] : next try { // handle undefined: i.e I forgot to add it to a/oTests if (undefined !== pName) { dom.navprev.innerHTML = '<a class="return" href="'+ previous +'.html">◀ ' + pName +'</a>' } if (undefined !== nName) { dom.navnext.innerHTML = '<a class="return" href="'+ next +'.html">'+ nName +' ▶</a>' } } catch(e) {} } function copyclip(element) { // fallback: e.g FF62- function copyExec() { if (document.selection) { let range = document.body.createTextRange() range.moveToElementText(document.getElementById(element)) range.select().createTextRange() document.execCommand("copy") } else if (window.getSelection) { let range = document.createRange() range.selectNode(document.getElementById(element)) window.getSelection().addRange(range) document.execCommand("copy") } } // clipboard API if ("clipboard" in navigator) { try { let content = document.getElementById(element).innerHTML // remove spans, change linebreaks let regex = /<br\s*[\/]?>/gi content = content.replace(regex, "\r\n") content = content.replace(/<\/?span[^>]*>/g,"") // get it navigator.clipboard.writeText(content).then(function() { // clipboard successfully set }, function() { // clipboard write failed copyExec() }) } catch(e) { copyExec() } } else { copyExec() } } function hide_overlays() { dom.modaloverlay.style.display = "none" dom.overlay.style.display = "none" } function show_overlay() { // this just displays it, test PoCs wil need to populate it beforehabd dom.modaloverlay.style.display = "block" dom.overlay.style.display = "block" } function showDetail(name) { if (name == "all") { console.log("ALL", sDetail) } else { let data = sDetail[name] let hash = mini(data) // split+tidy name name = name.replace(/\_/g, " ") let n = name.indexOf(" "), section = name.substring(0,n).toUpperCase(), metric = name.substring(n,name.length).trim() console.log(section +": "+ metric +": "+ hash, data) } } function showhide(id, style) { let items = document.getElementsByClassName("tog"+ id) for (let i=0; i < items.length; i++) {items[i].style.display = style} } function togglerows(id, word) { let items = document.getElementsByClassName("tog"+ id) let style = items[0].style.display == "table-row" ? "none" : "table-row" for (let i=0; i < items.length; i++) {items[i].style.display = style} if (word == "btn") { word = "[ "+ (style == "none" ? "show" : "hide") +" ]" } else if (word == "expand") { word = "[ "+ (style == "none" ? "expand" : "collapse") +" ]" } else { word = (style == "none" ? "&#9660; show " : "&#9650; hide ") + (word == "" || word == undefined ? "details" : word) } try {document.getElementById("label"+ id).innerHTML = word} catch(e) {} } if (location.protocol == "file:") {isFile = true} ================================================ FILE: tests/testglobals.js ================================================ 'use strict'; var dom; let sDetail = {} // css let s0 = "<span class='", sb = s0+"bad'>", sg = s0+"good'>", sf = s0+"faint'>", sn = s0+"neutral'>", snc = s0+"no_color'>", s1 = s0+"s1'>", s2 = s0+"s2'>", s3 = s0+"s3'>", s4 = s0+"s4'>", s5 = s0+"s5'>", s6 = s0+"s6'>", s7 = s0+"s7'>", s8 = s0+"s8'>", s9 = s0+"s9'>", s10 = s0+"s10'>", s11 = s0+"s11'>", s12 = s0+"s12'>", s13 = s0+"s13'>", s14 = s0+"s14'>", s15 = s0+"s15'>", s16 = s0+"s16'>", s17 = s0+"s17'>", s18 = s0+"s18'>", s99 = s0+"s99'>", sc = "</span>", // test icons green_tick = "<span style='font-size: 10px;'><b>" + s9.trim() +" \u2713"+ sc + "</b></span>", red_cross = "<span style='font-size: 10px;'><b>" + sb.trim() +" \u2715"+ sc + "</b></span>", yellow_block = "<span style='font-size: 10px;'><b>" + s4.trim() +" \u2715"+ sc + "</b></span>", white_na = "<span style='font-size: 10px;'><b>" + snc.trim() +" \u2715"+ sc + "</b></span>", // common results zErr = 'error', zNA = 'n/a', zU = 'undefined', zUQ = "\"undefined\"", zNEW = sb+'[NEW]'+sc, // other is95 = false, isEngine = '', isEnginePretty = '', // results string with perf isFF = false, isFFpretty = '', // results string with perf isFFvalid = false, // no errors isFile = false, isVer = '', isVerMax = '' // timezones let gTimezones = [ // no regional prefix 'CET','CST6CDT','Cuba','EET','EST','EST5EDT','Egypt','Eire','Factory','GB','GB-Eire','GMT','GMT+0','GMT-0','GMT0', 'Greenwich','HST','Hongkong','Iceland','Iran','Israel','Jamaica','Japan','Kwajalein','Libya','MET','MST','MST7MDT', 'NZ','NZ-CHAT','Navajo','PRC','PST8PDT','Poland','Portugal','ROC','ROK','Singapore','Turkey','UCT','UTC','Universal', 'W-SU','WET','Zulu', // THE REST 'Africa/Abidjan','Africa/Accra','Africa/Addis_Ababa','Africa/Algiers','Africa/Asmara','Africa/Asmera','Africa/Bamako', 'Africa/Bangui','Africa/Banjul','Africa/Bissau','Africa/Blantyre','Africa/Brazzaville','Africa/Bujumbura','Africa/Cairo', 'Africa/Casablanca','Africa/Ceuta','Africa/Conakry','Africa/Dakar','Africa/Dar_es_Salaam','Africa/Djibouti','Africa/Douala', 'Africa/El_Aaiun','Africa/Freetown','Africa/Gaborone','Africa/Harare','Africa/Johannesburg','Africa/Juba','Africa/Kampala', 'Africa/Khartoum','Africa/Kigali','Africa/Kinshasa','Africa/Lagos','Africa/Libreville','Africa/Lome','Africa/Luanda', 'Africa/Lubumbashi','Africa/Lusaka','Africa/Malabo','Africa/Maputo','Africa/Maseru','Africa/Mbabane','Africa/Mogadishu', 'Africa/Monrovia','Africa/Nairobi','Africa/Ndjamena','Africa/Niamey','Africa/Nouakchott','Africa/Ouagadougou', 'Africa/Porto-Novo','Africa/Sao_Tome','Africa/Timbuktu','Africa/Tripoli','Africa/Tunis','Africa/Windhoek', 'America/Adak','America/Anchorage','America/Anguilla','America/Antigua','America/Araguaina','America/Argentina/Buenos_Aires', 'America/Argentina/Catamarca','America/Argentina/ComodRivadavia','America/Argentina/Cordoba','America/Argentina/Jujuy', 'America/Argentina/La_Rioja','America/Argentina/Mendoza','America/Argentina/Rio_Gallegos','America/Argentina/Salta', 'America/Argentina/San_Juan','America/Argentina/San_Luis','America/Argentina/Tucuman','America/Argentina/Ushuaia', 'America/Aruba','America/Asuncion','America/Atikokan','America/Atka','America/Bahia','America/Bahia_Banderas', 'America/Barbados','America/Belem','America/Belize','America/Blanc-Sablon','America/Boa_Vista','America/Bogota', 'America/Boise','America/Buenos_Aires','America/Cambridge_Bay','America/Campo_Grande','America/Cancun','America/Caracas', 'America/Catamarca','America/Cayenne','America/Cayman','America/Chicago','America/Chihuahua','America/Ciudad_Juarez', 'America/Coral_Harbour','America/Cordoba','America/Costa_Rica','America/Creston','America/Cuiaba','America/Curacao', 'America/Danmarkshavn','America/Dawson','America/Dawson_Creek','America/Denver','America/Detroit','America/Dominica', 'America/Edmonton','America/Eirunepe','America/El_Salvador','America/Ensenada','America/Fort_Nelson','America/Fort_Wayne', 'America/Fortaleza','America/Glace_Bay','America/Godthab','America/Goose_Bay','America/Grand_Turk','America/Grenada', 'America/Guadeloupe','America/Guatemala','America/Guayaquil','America/Guyana','America/Halifax','America/Havana', 'America/Hermosillo','America/Indiana/Indianapolis','America/Indiana/Knox','America/Indiana/Marengo', 'America/Indiana/Petersburg','America/Indiana/Tell_City','America/Indiana/Vevay','America/Indiana/Vincennes', 'America/Indiana/Winamac','America/Indianapolis','America/Inuvik','America/Iqaluit','America/Jamaica','America/Jujuy', 'America/Juneau','America/Kentucky/Louisville','America/Kentucky/Monticello','America/Knox_IN','America/Kralendijk', 'America/La_Paz','America/Lima','America/Los_Angeles','America/Louisville','America/Lower_Princes','America/Maceio', 'America/Managua','America/Manaus','America/Marigot','America/Martinique','America/Matamoros','America/Mazatlan', 'America/Mendoza','America/Menominee','America/Merida','America/Metlakatla','America/Mexico_City','America/Miquelon', 'America/Moncton','America/Monterrey','America/Montevideo','America/Montreal','America/Montserrat','America/Nassau', 'America/New_York','America/Nipigon','America/Nome','America/Noronha','America/North_Dakota/Beulah', 'America/North_Dakota/Center','America/North_Dakota/New_Salem','America/Nuuk','America/Ojinaga','America/Panama', 'America/Pangnirtung','America/Paramaribo','America/Phoenix','America/Port-au-Prince','America/Port_of_Spain', 'America/Porto_Acre','America/Porto_Velho','America/Puerto_Rico','America/Punta_Arenas','America/Rainy_River', 'America/Rankin_Inlet','America/Recife','America/Regina','America/Resolute','America/Rio_Branco','America/Rosario', 'America/Santa_Isabel','America/Santarem','America/Santiago','America/Santo_Domingo','America/Sao_Paulo', 'America/Scoresbysund','America/Shiprock','America/Sitka','America/St_Barthelemy','America/St_Johns','America/St_Kitts', 'America/St_Lucia','America/St_Thomas','America/St_Vincent','America/Swift_Current','America/Tegucigalpa', 'America/Thule','America/Thunder_Bay','America/Tijuana','America/Toronto','America/Tortola','America/Vancouver', 'America/Virgin','America/Whitehorse','America/Winnipeg','America/Yakutat','America/Yellowknife', 'America/Coyhaique', // tzdata2025b 'Antarctica/Casey','Antarctica/Davis','Antarctica/DumontDUrville','Antarctica/Macquarie','Antarctica/Mawson', 'Antarctica/McMurdo','Antarctica/Palmer','Antarctica/Rothera','Antarctica/South_Pole','Antarctica/Syowa', 'Antarctica/Troll','Antarctica/Vostok','Arctic/Longyearbyen', 'Asia/Aden','Asia/Almaty','Asia/Amman','Asia/Anadyr','Asia/Aqtau','Asia/Aqtobe','Asia/Ashgabat','Asia/Ashkhabad', 'Asia/Atyrau','Asia/Baghdad','Asia/Bahrain','Asia/Baku','Asia/Bangkok','Asia/Barnaul','Asia/Beirut','Asia/Bishkek', 'Asia/Brunei','Asia/Calcutta','Asia/Chita','Asia/Choibalsan','Asia/Chongqing','Asia/Chungking','Asia/Colombo', 'Asia/Dacca','Asia/Damascus','Asia/Dhaka','Asia/Dili','Asia/Dubai','Asia/Dushanbe','Asia/Famagusta','Asia/Gaza', 'Asia/Harbin','Asia/Hebron','Asia/Ho_Chi_Minh','Asia/Hong_Kong','Asia/Hovd','Asia/Irkutsk','Asia/Istanbul', 'Asia/Jakarta','Asia/Jayapura','Asia/Jerusalem','Asia/Kabul','Asia/Kamchatka','Asia/Karachi','Asia/Kashgar', 'Asia/Kathmandu','Asia/Katmandu','Asia/Khandyga','Asia/Kolkata','Asia/Krasnoyarsk','Asia/Kuala_Lumpur','Asia/Kuching', 'Asia/Kuwait','Asia/Macao','Asia/Macau','Asia/Magadan','Asia/Makassar','Asia/Manila','Asia/Muscat','Asia/Nicosia', 'Asia/Novokuznetsk','Asia/Novosibirsk','Asia/Omsk','Asia/Oral','Asia/Phnom_Penh','Asia/Pontianak','Asia/Pyongyang', 'Asia/Qatar','Asia/Qostanay','Asia/Qyzylorda','Asia/Rangoon','Asia/Riyadh','Asia/Saigon','Asia/Sakhalin', 'Asia/Samarkand','Asia/Seoul','Asia/Shanghai','Asia/Singapore','Asia/Srednekolymsk','Asia/Taipei','Asia/Tashkent', 'Asia/Tbilisi','Asia/Tehran','Asia/Tel_Aviv','Asia/Thimbu','Asia/Thimphu','Asia/Tokyo','Asia/Tomsk','Asia/Ujung_Pandang', 'Asia/Ulaanbaatar','Asia/Ulan_Bator','Asia/Urumqi','Asia/Ust-Nera','Asia/Vientiane','Asia/Vladivostok','Asia/Yakutsk', 'Asia/Yangon','Asia/Yekaterinburg','Asia/Yerevan', 'Atlantic/Azores','Atlantic/Bermuda','Atlantic/Canary','Atlantic/Cape_Verde','Atlantic/Faeroe','Atlantic/Faroe', 'Atlantic/Jan_Mayen','Atlantic/Madeira','Atlantic/Reykjavik','Atlantic/South_Georgia','Atlantic/St_Helena', 'Atlantic/Stanley', 'Australia/ACT','Australia/Adelaide','Australia/Brisbane','Australia/Broken_Hill','Australia/Canberra','Australia/Currie', 'Australia/Darwin','Australia/Eucla','Australia/Hobart','Australia/LHI','Australia/Lindeman','Australia/Lord_Howe', 'Australia/Melbourne','Australia/NSW','Australia/North','Australia/Perth','Australia/Queensland','Australia/South', 'Australia/Sydney','Australia/Tasmania','Australia/Victoria','Australia/West','Australia/Yancowinna', 'Brazil/Acre','Brazil/DeNoronha','Brazil/East','Brazil/West', 'Canada/Atlantic','Canada/Central','Canada/Eastern','Canada/Mountain','Canada/Newfoundland','Canada/Pacific', 'Canada/Saskatchewan','Canada/Yukon', 'Chile/Continental','Chile/EasterIsland', 'Etc/GMT','Etc/GMT+0','Etc/GMT+1','Etc/GMT+10','Etc/GMT+11','Etc/GMT+12','Etc/GMT+2','Etc/GMT+3','Etc/GMT+4', 'Etc/GMT+5','Etc/GMT+6','Etc/GMT+7','Etc/GMT+8','Etc/GMT+9','Etc/GMT-0','Etc/GMT-1','Etc/GMT-10','Etc/GMT-11', 'Etc/GMT-12','Etc/GMT-13','Etc/GMT-14','Etc/GMT-2','Etc/GMT-3','Etc/GMT-4','Etc/GMT-5','Etc/GMT-6','Etc/GMT-7', 'Etc/GMT-8','Etc/GMT-9','Etc/GMT0','Etc/Greenwich','Etc/UCT','Etc/UTC','Etc/Universal','Etc/Zulu', 'Europe/Amsterdam','Europe/Andorra','Europe/Astrakhan','Europe/Athens','Europe/Belfast','Europe/Belgrade','Europe/Berlin', 'Europe/Bratislava','Europe/Brussels','Europe/Bucharest','Europe/Budapest','Europe/Busingen','Europe/Chisinau', 'Europe/Copenhagen','Europe/Dublin','Europe/Gibraltar','Europe/Guernsey','Europe/Helsinki','Europe/Isle_of_Man', 'Europe/Istanbul','Europe/Jersey','Europe/Kaliningrad','Europe/Kiev','Europe/Kirov','Europe/Kyiv','Europe/Lisbon', 'Europe/Ljubljana','Europe/London','Europe/Luxembourg','Europe/Madrid','Europe/Malta','Europe/Mariehamn','Europe/Minsk', 'Europe/Monaco','Europe/Moscow','Europe/Nicosia','Europe/Oslo','Europe/Paris','Europe/Podgorica','Europe/Prague','Europe/Riga', 'Europe/Rome','Europe/Samara','Europe/San_Marino','Europe/Sarajevo','Europe/Saratov','Europe/Simferopol','Europe/Skopje', 'Europe/Sofia','Europe/Stockholm','Europe/Tallinn','Europe/Tirane','Europe/Tiraspol','Europe/Ulyanovsk','Europe/Uzhgorod', 'Europe/Vaduz','Europe/Vatican','Europe/Vienna','Europe/Vilnius','Europe/Volgograd','Europe/Warsaw','Europe/Zagreb', 'Europe/Zaporozhye','Europe/Zurich', 'Indian/Antananarivo','Indian/Chagos','Indian/Christmas','Indian/Cocos','Indian/Comoro','Indian/Kerguelen', 'Indian/Mahe','Indian/Maldives','Indian/Mauritius','Indian/Mayotte','Indian/Reunion', 'Mexico/BajaNorte','Mexico/BajaSur','Mexico/General', 'Pacific/Apia','Pacific/Auckland','Pacific/Bougainville','Pacific/Chatham','Pacific/Chuuk','Pacific/Easter','Pacific/Efate', 'Pacific/Enderbury','Pacific/Fakaofo','Pacific/Fiji','Pacific/Funafuti','Pacific/Galapagos','Pacific/Gambier', 'Pacific/Guadalcanal','Pacific/Guam','Pacific/Honolulu','Pacific/Johnston','Pacific/Kanton','Pacific/Kiritimati', 'Pacific/Kosrae','Pacific/Kwajalein','Pacific/Majuro','Pacific/Marquesas','Pacific/Midway','Pacific/Nauru', 'Pacific/Niue','Pacific/Norfolk','Pacific/Noumea','Pacific/Pago_Pago','Pacific/Palau','Pacific/Pitcairn','Pacific/Pohnpei', 'Pacific/Ponape','Pacific/Port_Moresby','Pacific/Rarotonga','Pacific/Saipan','Pacific/Samoa','Pacific/Tahiti', 'Pacific/Tarawa','Pacific/Tongatapu','Pacific/Truk','Pacific/Wake','Pacific/Wallis','Pacific/Yap', 'US/Alaska','US/Aleutian','US/Arizona','US/Central','US/East-Indiana','US/Eastern','US/Hawaii','US/Indiana-Starke', 'US/Michigan','US/Mountain','US/Pacific','US/Samoa' ] // language/locale tests var gLocales = [ // ISO_639-1 alpha-2 codes: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes // ISO_639-2 alpha-3 codes: https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes // not supported in FF // but keep just in case: we check SupportedLocalesOf in each Intl constructor anyway 'aa,afar', 'ab,abkhazian', 'ae,avestan', 'an,aragonese', 'av,avaric', 'ay,aymara', 'ba,bashkir', 'bal,baluchi', 'bcg,haryanvi', 'bi,bislama', 'bqi,luri bakhtiari', // 2020533 'cak,kaqchikel', 'ch,chamorro', 'cho,choctaw', 'co,corsican', 'cr,cree', 'cu,church slavic', 'dv,divehi', 'fj,fijian', 'gn,guarani', 'ho,hiri motu', 'ht,haitian', 'hz,herero', 'ik,inupiaq', 'io,ido', 'iu,inuktitut', 'kg,kongo', 'kj,kuanyama', 'kr,kanuri', 'kv,komi', 'la,latin', 'li,limburgan', 'ltg,latgalian', 'meh,mixtec (southwestern tlaxiaco)', 'mh,marshallese', 'mix,mixtepec mixtec', 'na,nauru', 'ng,ndonga', 'nr,south ndebele', 'nv,navajo', 'ny,chichewa', 'oj,ojibwa', 'pap,papiamento', 'pi,pali', 'sco,scots', 'skr,saraiki', 'sm,samoan', 'ss,siswati', 'trs,triqui', 'ts,tsonga', 'ty,tahitian', 've,venda', 'vo,volapük', 'wa,walloon', // supported in FF140+ 'af,afrikaans', 'agq,aghem', 'ak,akan', 'am,amharic', 'ar,arabic', 'as,assamese', 'asa,asu', 'ast,asturian', 'az,azerbaijani', 'bas,basaa', 'be,belarusian', 'bem,bemba', 'bez,bena', 'bg,bulgarian', 'bgc,haryanvi', 'bho,bhojpuri', 'blo,anii', 'bm,bambara', 'bn,bengali', 'bo,tibetan', 'br,breton', 'brx,bodo', 'bs,bosnian', 'ca,catalan', 'ccp,chakma', 'ce,chechen', 'ceb,cebuano', 'cgg,chiga', 'chr,cherokee', 'ckb,central kurdish', 'cs,czech', 'csw,swampy', 'cv,chuvash', 'cy,welsh', 'da,danish', 'dav,taita', 'de,german', 'dje,zarma', 'doi,dogri', 'dsb,lower sorbian', 'dua,duala', 'dyo,jola-fonyi', 'dz,dzongkha', 'ebu,embu', 'ee,éwé', 'el,greek', 'en,english', 'eo,esperanto', 'es,spanish', 'et,estonian', 'eu,basque', 'ewo,ewondo', 'fa,persian', 'ff,fulah', 'fi,finnish', 'fil,filipino', 'fo,faroese', 'fr,french', 'fur,friulian', 'fy,frisian', 'ga,irish', 'gaa,ga', 'gd,scottish gaelic', 'gl,galician', 'gsw,swiss german', 'gu,gujarati', 'guz,gusii', 'gv,manx', 'ha,hausa', 'haw,hawaiian', 'he,hebrew', 'hi,hindi', 'hr,croatian', 'hsb,upper sorbian', 'hu,hungarian', 'hy,armenian', 'ia,interlingua', 'id,indonesian', 'ie,interlingue', 'ig,igbo', 'ii,sichuan yi', 'is,icelandic', 'it,italian', 'ja,japanese', 'jgo,ngomba', 'jmc,machame', 'jv,javanese', 'ka,georgian', 'kab,kabyle', 'kam,kamba', 'kde,makonde', 'kea,kabuverdianu', 'kgp,kaingang', 'khq,koyra chiini', 'ki,kikuyu', 'kk,kazakh', 'kkj,kako', 'kl,greenlandic', 'kln,kalenjin', 'km,khmer', 'kn,kannada', 'ko,korean', 'kok,konkani', 'ks,kashmiri', 'ksb,shambala', 'ksf,bafia', 'ksh,colognian', 'ku,kurdish', 'kw,cornish', 'kxv,kuvi', 'ky,kyrgyz', 'lag,langi', 'lb,luxembourgish', 'lg,ganda', 'lij,ligurian', 'lkt,lakota', 'lmo,lombard', 'ln,lingala', 'lo,lao', 'lrc,northern luri', 'lt,lithuanian', 'lu,luba-katanga', 'luo,luo', 'luy,luyia', 'lv,latvian', 'mai,maithili', 'mas,maasai', 'mer,meru', 'mfe,morisyen', 'mg,malagasy', 'mgh,makhuwa-meetto', 'mgo,meta\'', 'mi,maori', 'mk,macedonian', 'ml,malayalam', 'mn,mongolian', 'mni,meitei', 'mr,marathi', 'ms,malay', 'mt,maltese', 'mua,mundang', 'my,burmese', 'mzn,mazanderani', 'naq,nama', 'nb,norwegian bokmål', 'nd,north ndebele', 'nds,low german', 'ne,nepali', 'nl,dutch', 'nmg,kwasio', 'nn,norwegian nynorsk', 'nnh,ngiemboon', 'no,norwegian', 'nqo,n’ko', 'nso,northern sotho', 'nus,nuer', 'nyn,nyankole', 'oc,occitan', 'om,oromo', 'or,odia', 'os,ossetian', 'pa,punjabi', 'pcm,nigerian', 'pl,polish', 'prg,prussian', 'ps,pashto', 'pt,portuguese', 'qu,quechua', 'raj,rajasthani', 'rm,rhaeto-romanic', 'rn,kirundi', 'ro,romanian', 'rof,rombo', 'ru,russian', 'rw,kinyarwanda', 'rwk,rwa', 'sa,sanskrit', 'sah,yakut', 'saq,samburu', 'sat,santali', 'sbp,sangu', 'sc,sardinian', 'sd,sindhi', 'se,northern sami', 'seh,sena', 'ses,koyraboro senni', 'sg,sango', 'shi,tachelhit', 'si,sinhala', 'sk,slovak', 'sl,slovenian', 'smn,inari sámi', 'sn,shona', 'so,somali', 'sq,albanian', 'sr,serbian', 'st,southern sotho', 'su,sundanese', 'sv,swedish', 'sw,swahili', 'syr,syriac', 'szl,silesian', 'ta,tamil', 'te,telugu', 'teo,teso', 'tg,tajik', 'th,thai', 'ti,tigrinya', 'tk,turkmen', 'tn,tswana', 'to,tongan', 'tok,toki pona', 'tr,turkish', 'tt,tatar', 'twq,tasawaq', 'tzm,central atlas tamazight', 'ug,uighur', 'uk,ukrainian', 'ur,urdu', 'uz,uzbek', 'vai,vai', 'vec,venetian', 'vi,vietnamese', 'vmw,makhuwa', 'vun,vunjo', 'wae,walser', 'wo,wolof', 'xh,xhosa', 'xnr,kangri', 'xog,soga', 'yav,yangben', 'yi,yiddish', 'yo,yoruba', 'yrl,nhengatu', 'yue,cantonese', 'za,zhuang', 'zgh,standard moroccan tamazight', 'zh,chinese', 'zh-cn,chinese (china)', 'zh-hans,chinese (simplified)', 'zh-hant,chinese (traditional)', 'zh-hk,chinese (hong kong)', 'zh-sg,chinese (singapore)', 'zh-tw,chinese (taiwan)', 'zu,zulu', ] var gLocalesOriginal = gLocales // ** local files only for dev tests ** // let gLocalesLikely = [ // the usual suspects //* 'af-na,afrikaans (namibia)', 'ar-ae,arabic (united arabic emirates)', 'ar-bh,arabic (bahrain)', 'ar-dj,arabic (djibouti)', 'ar-dz,arabic (algeria)', 'ar-eg,arabic (egypt)', 'ar-eh,arabic (western sahara)', 'ar-er,arabic (eritrea)', 'ar-il,arabic (israel)', 'ar-iq,arabic (iraq)', 'ar-km,arabic (cosmoros)', 'ar-lb,arabic (lebanon)', 'ar-ly,arabic (libya)', 'ar-ma,arabic (morocco)', 'ar-mr,arabic (mauritania)', 'ar-sa,arabic (saudi arabia)', 'ar-so,arabic (somalia)', 'ar-ss,arabic (south sudan)', 'ar-tn,arabic (tunisia)', 'az-cyrl,azerbaijani (cyrillic)', 'bn-in,bengali (india)', 'bo-in,tibetan (india)', 'bs-cyrl,bosnian (cyrillic)', 'ca-fr,catalan (france)', 'ckb-ir,central kurdish (iran)', 'de-at,german (austria)', 'de-ch,german (switzerland)', 'de-li,german (liechtenstein)', 'de-lu,german (luxembourg)', 'dyo-sn,jola-fonyl (senegal)', 'ee-tg,éwé (togo)', 'en-001,english', 'en-150,english (europe)', 'en-ae,english (united arab emirates)', 'en-ag,english (antigua & barbuda)', 'en-ai,english (anguilla)', 'en-at,english (austria)', 'en-au,english (australia)', 'en-bb,english (barbados)', 'en-be,english (belgium)', 'en-bi,english (burundi)', 'en-bm,english (bermuda)', 'en-bs,english (bahamas)', 'en-bw,english (botswana)', 'en-bz,english (belize)', 'en-ca,english (canada)', 'en-cc,english (cocos islands)', 'en-ch,english (switzerland)', 'en-dk,english (denmark)', 'en-er,english (eritrea)', 'en-fi,english (finland)', 'en-fj,english (fiji)', 'en-fk,english (falkland islands)', 'en-gb,english (united kingdom)', 'en-gg,english (guernsey)', 'en-gh,english (ghana)', 'en-gi,english (gibraltar)', 'en-gm,english (gambia)', 'en-gu,english (guam)', 'en-gy,english (guyana)', 'en-hk,english (hong kong)', 'en-ie,english (ireland)', 'en-il,english (israel)', 'en-in,english (india)', 'en-jm,english (jamaica)', 'en-ke,english (kenya)', 'en-ky,english (cayman islands)', 'en-lr,english (liberia)', 'en-ls,english (lesotho)', 'en-mg,english (madagascar)', 'en-mh,english (marshall islands)', 'en-mo,english (macau)', 'en-mt,english (malta)', 'en-mu,english (mauritius)', 'en-mw,english (malawai)', 'en-my,english (malaysia)', 'en-na,english (namibia)', 'en-ng,english (nigeria)', 'en-nz,english (new zealand)', 'en-pg,english (papua new guinea)', 'en-pk,english (pakistan)', 'en-pr,english (puerto rico)', 'en-rw,english (rwanda)', 'en-sb,english (solomon islands)', 'en-sc,english (seychelles)', 'en-se,english (sweden)', 'en-sg,english (singapore)', 'en-sh,english (saint helena)', 'en-sl,english (sierra leone)', 'en-ss,english (south sudan)', 'en-sx,english (sint maarten)', 'en-sz,english (swaziland)', 'en-to,english (tonga)', 'en-tt,english (trinidad & tobago)', 'en-tz,english (tanzania)', 'en-ug,english (uganda)', 'en-vu,english (vanuatu)', 'en-ws,english (samoa)', 'en-za,english (south africa)', 'en-zm,english (zambia)', 'en-zw,english (zimbabwe)', 'es-419,spanish (latin america and the caribbean)', 'es-ar,spanish (argentina)', 'es-bo,spanish (bolivia)', 'es-br,spanish (brazil)', 'es-bz,spanish (belize)', 'es-cl,spanish (chile)', 'es-co,spanish (colombia)', 'es-cr,spanish (costa rica)', 'es-cu,spanish (cuba)', 'es-do,spanish (dominican republic)', 'es-ec,spanish (ecuador)', 'es-gq,spanish (equatorial guinea)', 'es-gt,spanish (guatemala)', 'es-hn,spanish (honduras)', 'es-mx,spanish (mexico)', 'es-ni,spanish (nicaragua)', 'es-pa,spanish (panama)', 'es-pe,spanish (peru)', 'es-ph,spanish (philippines)', 'es-pr,spanish (puerto rico)', 'es-py,spanish (paraguay)', 'es-sv,spanish (el salvador)', 'es-us,spanish (united states)', 'es-uy,spanish (uruguay)', 'es-ve,spanish (venezuela)', 'fa-af,persian (afghanistan)', 'ff-adlm,fulah (adlam)', 'ff-adlm-bf,fulah (adlam burkina faso)', 'ff-adlm-gh,fulah (adlam ghana)', 'ff-adlm-gm,fulah (adlam gambia)', 'ff-adlm-lr,fulah (adlamd liberia)', 'ff-adlm-mr,fulah (adlam mauritania)', 'ff-adlm-ng,fulah (adlam nigeria)', 'ff-adlm-sl,fulah (adlam sierra leone)', 'ff-gh,fulah (ghana)', 'ff-gn,fulah (guinea)', 'ff-mr,fulah (mauritania)', 'fo-dk,faroese (denmark)', 'fr-be,french (belgium)', 'fr-bi,french (burundi)', 'fr-ca,french (canada)', 'fr-cd,french (congo kinshasa)', 'fr-ch,french (switzerland)', 'fr-cm,french (cameroon)', 'fr-dj,french (djibouti)', 'fr-dz,french (algeria)', 'fr-gf,french (french guiana)', 'fr-gn,french (guinea)', 'fr-gp,french (guadeloupe)', 'fr-ht,french (haiti)', 'fr-km,french (comoros)', 'fr-lu,french (luxembourg)', 'fr-ma,french (morocco)', 'fr-ml,french (mali)', 'fr-mg,french (madagascar)', 'fr-mr,french (mauritania)', 'fr-mu,french (mauritius)', 'fr-rw,french (rwanda)', 'fr-sc,french (seychelles)', 'fr-sn,french (senegal)', 'fr-sy,french (syria)', 'fr-tn,french (tunisia)', 'fr-vu,french (vanuatu)', 'ha-gh,hausa (ghana)', 'hi-latn,hindi (latin)', 'hr-ba,croatian (bosnia & herzegovina)', 'it-ch,italian (switzerland)', 'kea-cv,kabuverdianu (cape verde)', 'kk-cn,kazakh (china)', 'ko-kp,korean (north korea)', 'kok-in,konkani (india)', 'kok-latn,konkani (latin)', 'knn-knda,konkani (kannada)', // 'knn-latn,konkani (latin)', // 'ks-deva,kashmiri (devanagari)', 'kxv-telu,kuvi (telugu)', 'ln-ao,lingala (angola)', 'lrc-iq,northern luri (iraq)', 'mas-tz,masia (tanzania)', 'ms-bn,malay (brunei)', 'ms-id,malay (indonesia)', 'ms-sg,malay (singapore)', 'ne-in,nepali (india)', 'nl-aw,dutch (aruba)', 'nl-be,dutch (belgium)', 'nl-bq,dutch (caribbean netherlands)', 'nl-cw,dutch (curaçao)', 'nl-sr,dutch (suriname)', 'om-ke,oromo (kenya)', 'os-ru,ossetian (russia)', 'pa-arab,punjabi (arabic)', 'pa-pk,punjabi (pakistan)', 'ps-pk,pashto (pakistan)', 'pt-ao,portuguese (angola)', 'pt-ch,portuguese (switzerland)', 'pt-cv,portuguese (cape verde)', 'pt-gw,portuguese (guinea-bissau)', 'pt-lu,portuguese (luxembourg)', 'pt-mo,portuguese (macau)', 'pt-mz,portuguese (mazambique)', 'pt-pt,portuguese (portugal)', 'pt-st,portuguese (são tomé & príncipe)', 'qu-bo,quechua (bolivia)', 'qu-ec,quechua (ecuador)', 'ro-md,romanian (moldova)', 'ru-by,russian (belarus)', 'ru-kg,russian (kyrgyzstan)', 'ru-kz,russian (kazakhstan)', 'ru-md,russian (moldova)', 'ru-ua,russian (ukraine)', 'sd-deva,sindhi (devanagari)', 'se-fi,northern sami (finland)', 'se-se,northern sami', 'shi-latn,tachelhit (latin)', 'so-dj,somali (djibouti)', 'so-et,somali (ethiopia)', 'so-ke,somali (kenya)', 'sq-mk,albanian (macedonia)', 'sr-ba,serbian (bosnia & herzegovina)', 'sr-cyrl-ba,serbian (cyrillic bosnia & herzegovina)', 'sr-cyrl-me,serbian (cyrillic montenegro)', 'sr-cyrl-xk,serbian (cyrillic kosovo)', 'sr-latn,serbian (latin)', 'sr-latn-ba,serbian (latin bosnia & herzegovina)', 'sr-latn-me,serbian (latin montenegro)', 'sr-latn-xk,serbian (latin kosovo)', 'sr-me,serbian (montenegro)', 'sr-xk,serbian (kosovo)', 'st-ls,southern sotho', 'sv-ax,swedish (åland islands)', 'sv-fi,swedish (finland)', 'sw-cd,swahili (congo kinshasa)', 'sw-ke,swahili (kenya)', 'sw-ug,swahili (uganda)', 'ta-lk,tamil (sri lanka)', 'ta-my,tamil (malaysia)', 'ta-sg,tamil (singapore)', 'teo-ke,teso (kenya)', 'ti-er,tigrinya (eritrea)', 'tn-bw,tswana (botswana)', 'tr-cy,turkish (cyprus)', 'tr-tr,turkish (turkey)', 'ur-in,urdu (india)', 'uz-af,uzbek (afghanistan)', 'uz-arab,uzbek (arabic)', 'uz-cyrl-uz,uzbek (cyrillic uzbekistan)', 'vai-latn,vai (latin)', 'yo-bj,yoruba (benin)', 'yue-cn,cantonese (china)', 'yue-hans,cantonese (simplified)', 'zh-hans-hk,chinese (simplified hong kong)', 'zh-hans-mo,chinese (simplified macau)', 'zh-hant-mo,chinese (traditional macau)', 'zh-my,chinese (malaysia)', //*/ ] let gLocalesExpand = [ // unlikely //* 'af-arab,afrikaans (arabic)', 'af-brai,afrikaans (braille)', 'af-za,afrikaans (south africa)', 'agq-cm,aghem (cameroon)', 'ak-arab,akan (arabic)', 'ak-gh,akan (ghana)', 'ak-x-asante,akan', 'am-arab,amharic (arabic)', 'am-brai,amharic (braille)', 'am-et,amharic (ethiopic)', 'ar-001,arabic (world)', 'ar-brai,arabic (braille)', 'ar-jo,arabic (jordan)', 'ar-kw,arabic (kuwait)', 'ar-om,arabic (oman)', 'ar-ps,arabic (palestinian territories)', 'ar-qa,arabic (qatar)', 'ar-sd,arabic (sudan)', 'ar-sy,arabic (syria)', 'ar-syrc,arabic (syraic)', 'ar-td,arabic (chad)', 'ar-ye,arabic (yemen)', 'as-in,assamese (india)', 'asa-tz,asu (tanzania)', 'ast-es,asturian (spain)', 'az-brai,azerbaijani (braille)', 'az-cyrl-az,azerbaijani (cyrillic azerbaijan)', 'az-iq,azerbaijani (iraq)', 'az-ir,azerbaijani (iran)', 'az-latn,azerbaijani (latin)', 'az-ru,azerbaijani (russia)', 'bas-cm,basaa (cameroon)', 'be-arab,belarusian (arabic)', 'be-brai,belarusian (braille)', 'be-by,belarusian (belarus)', 'be-latn,belarusian', 'bem-brai,bemba (braille)', 'bem-zm,bemba (zambia)', 'bez-tz,bena (tanzania)', 'bg-bg,bulgarian (bulgaria)', 'bg-brai,bulgarian (braille)', 'bg-cyrs,bulgarian', 'bg-latn,bulgarian', 'bho-kthi,bhojpuri (kaithi)', 'bm-arab,bambara (arabic)', 'bm-ml,bambara (mali)', 'bm-nkoo,bambara', 'bn-bd,bengali (bangladesh)', 'bn-brai,bengali (braille)', 'bn-newa,bengali (newar)', 'bo-cn,tibetan (china)', 'bo-latn,tibetan', 'bo-marc,tibetan', 'bo-phag,tibetan', 'br-fr,breton (france)', 'br-ogam,breton', 'brx-beng,bodo (bengali)', 'brx-in,bodo (india)', 'brx-latn,bodo (latin)', 'bs-arab,bosnian (arabic)', 'bs-latn,bosnian (latin)', 'ca-ad,catalan (andorra)', 'ca-es,catalan (spain)', 'ca-it,catalan (italy)', 'ca-valencia,catalan (valencia)', 'ccp-bd,chakma (bangladesh)', 'ccp-beng,chakma (bengali)', 'ccp-in,chakma (india)', 'ccp-latn,chakma', 'ce-arab,chechen (arabic)', 'ce-latn,chechen', 'ce-ru,chechen (russia)', 'ceb-brai,cebuano (braille)', 'cgg-ug,chiga (uganda)', 'chr-latn,cherokee', 'chr-us,cherokee (united states)', 'ckb-iq,central kurdish (iraq)', 'ckb-latn,central kurdish', 'cs-brai,czech (braille)', 'cs-cz,czech (czechia)', 'cy-brai,welsh (braille)', 'cy-gb,welsh (united kingdom)', 'cy-ogam,welsh', 'da-brai,danish (braille)', 'da-dk,danish (denmark)', 'da-gl,danish (greenland)', 'da-runr,danish (runic)', 'dav-ke,taita (kenya)', 'de-be,german (belgium)', 'de-brai,german (braille)', 'de-de,german (germany)', 'de-dupl,german (duployan)', 'de-it,german (italy)', 'de-latf,german', 'de-runr,german (runic)', 'dje-arab,zarma (arabic)', 'dje-brai,zarma (braille)', 'dje-ne,zarma (niger)', 'doi-arab,dogri (arabic)', 'doi-dogr,dogri', 'doi-takr,dogri', 'dsb-de,lower sorbian (germany)', 'dua-cm,duala (cameroon)', 'dyo-arab,jola-fonyi (arabic)', 'dz-bt,dzongkha (bhutan)', 'ebu-ke,embu (kenya)', 'ee-brai,éwé (braille)', 'ee-gh,éwé (ghana)', 'el-brai,greek (braille)', 'el-cy,greek (cyprus)', 'el-cyrl,greek (cyrillic)', 'el-gr,greek (greece)', 'en-as,english (american samoa)', 'en-brai,english (braille)', 'en-ck,english (cook islands)', 'en-cm,english (cameroon)', 'en-cx,english (christmas island)', 'en-cy,english (cyprus)', 'en-de,english (germany)', 'en-dg,english (diego garcia)', 'en-dm,english (dominica)', 'en-dsrt,english (deseret)', 'en-dupl,english (duployan)', 'en-es,english (spain)', // ICU77 CLDR47 'en-fm,english (micronesia)', 'en-gd,english (grenada)', 'en-im,english (isle of man)', 'en-io,english (british indian oceam territory)', 'en-je,english (jersey)', 'en-ki,english (kiribati)', 'en-kn,english (saint kitts & nevis)', 'en-lc,english (saint lucia)', 'en-ma,english (morocco)', 'en-mp,english (northern mariana islands)', 'en-ms,english (montserrat)', 'en-nf,english (norfolk island)', 'en-nl,english (netherlands)', 'en-nr,english (nauru)', 'en-nu,english (niue)', 'en-ph,english (philippines)', 'en-pn,english (pitcarin islands)', 'en-pw,english (palau)', 'en-runr,english (runic)', 'en-sd,english (sudan)', 'en-shaw,english (shaw)', 'en-si,english (solvenia)', 'en-tc,english (turks & caicos islands)', 'en-tk,english (tokelau)', 'en-tv,english (tuvalu)', 'en-um,english (us outlying islands)', 'en-us,english (united states)', 'en-vc,english (saint vincent & grenadines)', 'en-vg,english (british virgin islands)', 'en-vi,english (us virgin islands)', 'es-brai,spanish (braille)', 'es-dupl,spanish (duployan)', 'es-ea,spanish (ceuta & melilla)', 'es-es,spanish (spain)', 'es-ic,spanish (canary islands)', 'et-brai,estonian (braille)', 'et-ee,estonian (estonia)', 'eu-es,basque (spain)', 'ewo-cm,ewondo (cameroon)', 'fa-ir,persian (iran)', 'ff-adlm-cm,fulah (adlam cameroon)', 'ff-adlm-gw,fulah (adlam guinea-bissau)', 'ff-adlm-ne,fulah (adlam niger)', 'ff-adlm-sn,fulah (adlam senegal)', 'ff-arab,fulah (arabic)', 'ff-bf,fulah (burkina faso)', 'ff-cm,fulah (cameroon)', 'ff-gm,fulah (gambia)', 'ff-gw,fulah (guinea-bissau)', 'ff-lr,fulah (liberia)', 'ff-ne,fulah (niger)', 'ff-ng,fulah (nigeria)', 'ff-sl,fulah (sierra leone)', 'ff-sn,fulah (senegal)', 'fi-brai,finnish (braille)', 'fi-fi,finnish (finland)', 'fil-ph,filipino (philippines)', 'fil-brai,filipino (braille)', 'fil-buhd,filipino', 'fil-hano,filipino', 'fil-tglg,filipino (tagalog)', 'fo-fo,faroese (faroe islands)', 'fo-runr,faroese (runic)', 'fr-bf,french (burkina faso)', 'fr-bj,french (benin)', 'fr-bl,french (saint barthélemy)', 'fr-brai,french (braille)', 'fr-cf,french (central african republic)', 'fr-cg,french (congo brazzaville)', 'fr-ci,french (côte d’ivoire)', 'fr-dupl,french (duployan)', 'fr-fr,french (france)', 'fr-ga,french (gabon)', 'fr-gq,french (equatorial guinea)', 'fr-mc,french (monaco)', 'fr-mf,french (saint martin)', 'fr-mq,french (martinique)', 'fr-nc,french (new caledonia)', 'fr-ne,french (niger)', 'fr-pf,french (french polynesia)', 'fr-pm,french (saint pierre & miquelon)', 'fr-re,french (réunion)', 'fr-td,french (chad)', 'fr-tg,french (togo)', 'fr-wf,french (wallis & futuna)', 'fr-yt,french (mayotte)', 'fur-it,friulian (italy)', 'fy-nl,frisian (netherlands)', 'ga-ie,irish (ireland)', 'ga-latg,irish', 'ga-ogam,irish', 'gd-gb,scottish gaelic (united kingdom)', 'gd-ogam,scottish gaelic', 'gl-es,galician (spain)', 'gsw-ch,swiss german (switzerland)', 'gsw-fr,swiss german (france)', 'gsw-li,swiss german (liechtenstein)', 'gu-brai,gujarati (braille)', 'gu-in,gujarati (india)', 'gu-khoj,gujarati', 'gv-im,manx (isle of man)', 'gv-ogam,manx', 'ha-arab,hausa (arabic)', 'ha-brai,hausa (braille)', 'ha-cm,hausa (cameroon)', 'ha-latn-ne,hausa (latin niger)', 'ha-ne,hausa (niger)', 'ha-ng,hausa (nigeria)', 'ha-sd,hausa (sudan)', 'haw-us,hawaiian (united states)', 'he-brai,hebrew (braille)', 'he-il,hebrew (israel)', 'hi-brai,hindi (braille)', 'hi-in,hindi (india)', 'hi-mahj,hindi', 'hi-newa,hindi (newar)', 'hr-brai,croatian (braille)', 'hr-hr,croatian (croatia)', 'hsb-de,upper sorbian (germany)', 'hu-brai,hungarian (braille)', 'hu-hu,hungarian (hungary', 'hy-am,armenian (armenia)', 'hy-brai,armenian (braille)', 'id-arab,indonesian (arabic)', 'id-brai,indonesian (braille)', 'id-id,indonesian (indonesia)', 'ig-ng,igbo (niger)', 'ii-cn,sichuan yi (china)', 'ii-latn,sichuan yi (latin)', 'is-is,icelandic (iceland)', 'is-runr,icelandic (runic)', 'it-brai,italian (braille)', 'it-it,italian (italy)', 'it-sm,italian (san marino)', 'it-va,italian (vatican city)', 'ja-brai,japanese (braille)', 'ja-jp,japanese (japan)', 'ja-latn,japanese (latin)', 'jmc-tz,machame (tanzania)', 'jv-arab,javanese (arabic)', 'jv-java,javanese', 'ka-brai,georgian (braille)', 'ka-ge,georgian (georgia)', 'ka-geok,georgian', 'kab-arab,kabyle (arabic)', 'kab-dz,kabyle (algeria)', 'kab-tfng,amazigh (tifinagh)', 'kam-ke,kamba (kenya)', 'kde-tz,makonde (tanzania)', 'khq-ml,koyra chiini (mali)', 'ki-ke,kikuyu (kenya)', 'kk-af,kazakh (afghanistan)', 'kk-brai,kazakh (braille)', 'kk-ir,kazakh', 'kk-kz,kazakh (kazakhstan)', 'kk-latn,kazakh', 'kk-mn,kazakh', 'kkj-cm,kako (cameroon)', 'kkj-td,kako', 'kl-gl,greenlandic (greenland)', 'kln-ke,kalenjin (kenya)', 'km-kh,khmer (cambodia)', 'kn-brai,kannada (braille)', 'kn-in,kannada (india)', 'kn-nand,kannada', 'ko-brai,korean (braille)', 'ko-kr,korean (south korea)', 'ko-latn,korean (latin)', 'kok-knda,konkani (kannada)', 'ks-in,kashmiri (india)', 'ks-latn,kashmiri (latin)', 'ks-shrd,kashmiri', 'ksb-tz,shambala (tanzania)', 'ksf-cm,bafia (cameroon)', 'ksh-de,colognian (germany)', 'ku-arab,kurdish (arabic)', 'ku-arab-tr,kurdish (arabic)', 'ku-armn,kurdish', 'ku-cyrl,kurdish (cyrillic)', 'ku-cyrl-az,kurdish (azerbaijan)', 'ku-lb,kurdish (lebanon)', 'ku-yezi-ge,kurdish', 'kw-gb,cornish (united kingdom)', 'kw-ogam,cornish', 'ky-cn,kyrgyz (china)', 'ky-kg,kyrgyz (kyrgyzstan)', 'ky-tr,kyrgyz', 'lag-tz,langi (tanzania)', 'lb-lu,luxembourgish (luxembourg)', 'lg-arab,ganda (arabic)', 'lg-ug,ganda (uganda)', 'lkt-us,lakota (united states)', 'ln-cd,lingala (congo kinshasa)', 'ln-cf,lingala (central african republic)', 'ln-cg,lingala (congo brazzaville)', 'lo-la,lao (laos)', 'lrc-ir,northern luri (iran)', 'lt-latf,lithuanian', 'lt-lt,lithuanian (lithuania)', 'lu-cd,luba-katanga (congo kinshasa)', 'luo-ke,luo (kenya)', 'luy-ke,luyia (kenya)', 'lv-brai,latvian (braille)', 'lv-lv,latvian (latvia)', 'mai-kthi,maithili (kaithi)', 'mai-newa,maithili (newar)', 'mai-tirh,maithili (tirhuta)', 'mas-ke,masia (kenya)', 'mer-ke,meru (kenya)', 'mfe-mu,morisyen (mauritius)', 'mg-arab,malagasy (arabic)', 'mg-brai,malagasy (braille)', 'mg-mg,malagasy (madagascar)', 'mgh-mz,makhuwa-meetto (mozambique)', 'mgo-cm,meta\' (cameroon)', 'mk-brai,macedonian (braille)', 'mk-mk,macedonian (north macedonia)', 'ml-arab,malayalam (arabic)', 'ml-brai,malayalam (braille)', 'ml-in,malayalam (india)', 'mn-brai,mongolian (braille)', 'mn-cn,mongolian (china)', 'mn-mn,mongolian (mongolia)', 'mn-phag,mongolian', 'mn-phag-mn, halh', 'mn-tibt,mongolian', 'mni-brai,meitei (braille)', 'mni-mtei,meitei', 'mr-brai,marathi (braille)', 'mr-in,marathi (india)', 'mr-modi,marathi', 'ms-arab,malay (arabic)', 'ms-brai,malay (braille)', 'ms-cc,malay', 'ms-my,malay (malaysia)', 'mt-arab,maltese (arabic)', 'mt-brai,maltese (braille)', 'mt-mt,maltese (malta)', 'mua-cm,mundang (cameroon)', 'my-brai,burmese (braille)', 'my-mm,burmese (burma)', 'mzn-ir,mazanderani (iran)', 'naq-na,nama (namibia)', 'nb-no,norwegian bokmål (norway)', 'nb-sj,norwegian bokmål (svalbard & jan mayen)', 'nd-zw,north ndebele (zimbabwe)', 'ne-brai,nepali (braille)', 'ne-newa,nepali (newar)', 'ne-np,nepali (nepal)', 'nl-brai,dutch (braille)', 'nl-nl,dutch (netherlands)', 'nl-sx,dutch (sint maarten)', 'nmg-gq,kwasio', 'nn-no,norwegian nynorsk (norway', 'nnh-cm,ngiemboon (cameroon)', 'no-runr,norwegian (runic)', 'nus-sd,nuer (sudan)', 'nus-ss,nuer (south sudan)', 'nyn-ug,nyankole (uganda)', 'om-arab,oromo (arabic)', 'om-et,oromo (ethiopia)', 'om-ethi,oromo (ethiopic)', 'or-brai,odia (braille)', 'or-in,odia (india)', 'os-ge,ossetian (georgia)', 'os-geor,ossetian', 'os-latn,ossetian (latin)', 'pa-guru,punjabi (gurmukhi)', 'pa-in,punjabi (india)', 'pa-khoj,punjabi', 'pa-mahj,punjabi', 'pl-brai,polish (braille)', 'pl-pl,polish (poland)', 'pt-br,portuguese (brazil)', 'pt-brai,portuguese (braille)', 'pt-gq,portuguese (equatorial guinea)', 'pt-tl,portuguese (timor-leste)', 'qu-pe,quechua (peru)', 'raj-latn,rajasthani (latin)', 'rm-ch,rhaeto-romanic (switzerland)', 'rn-bi,kirundi (burundi)', 'rn-brai,rundi (braille)', 'ro-brai,romanian (braille)', 'ro-cyrl,romanian (cyrillic)', 'ro-cyrs,romanian', 'ro-dupl,romanian (duployan)', 'ro-ro,romanian (romania)', 'rof-tz,rombo (tanzania)', 'ru-brai,russian (braille)', 'ru-ru,russian (russia)', 'rw-brai,kinyarwanda (braille)', 'rw-rw,kinyarwanda (rwanda)', 'rwk-tz,rwa (tanzania)', 'sa-bhks,sanskrit', 'sa-gran,sanskrit', 'sa-kawi,sanskrit', 'sa-khar,sanskrit', 'sa-mymr,sanskrit', 'sa-nand,sanskrit', 'sa-newa,sanskrit', 'sa-shrd,sanskrit', 'sa-sidd,sanskrit', 'sa-sinh,sanskrit', 'sa-tirh,sanskrit (tirhuta)', 'sah-cyrs,yakut', 'sah-latn,yakut (latin)', 'sah-ru,yakut (russia)', 'saq-ke,samburu (kenya)', 'sat-beng,santali (bengali)', 'sat-deva,santali (devanagari)', 'sat-latn,santali (latin)', 'sat-orya,santali', 'sbp-tz,sangu (tanzania)', 'sd-guru,sindhi', 'sd-khoj,sindhi', 'sd-khoj-pk,sindhi', 'sd-sind,sindhi', 'se-cyrl,northern sami (cyrillic)', 'se-no,northern sami (norway)', 'se-sw,northern sami (sweden)', 'seh-mz,sena (mozambique)', 'seh-x-podzu,sena', 'ses-ml,koyraboro senni (mali)', 'sg-cf,sango (central african republicj)', 'shi-arab,tachelhit (arabic)', 'shi-tfng,tachelhit (tifinagh)', 'si-brai,sinhala (braille)', 'si-lk,sinhala (sri lanka)', 'sk-brai,slovak (braille)', 'sk-sk,slovak (slovakia)', 'sl-brai,slovenian (braille)', 'sl-si,slovenian (slovenia)', 'smn-fi,inari sámi (finland)', 'sn-brai,shona (braille)', 'sn-zw,shona (zimbabwe)', 'so-arab,somali (arabic)', 'so-osma,somali', 'so-so,somali (somalia)', 'sq-al,albanian (albania)', 'sq-brai,albanian (braille)', 'sq-elba,albanian', 'sq-grek,albanian', 'sq-vith,albanian', 'sq-xk,albanian (kosovo)', 'sr-brai,serbian (braille)', 'sr-cyrl,serbian (cyrillic)', 'sr-ro,serbian', 'sr-ru,serbian', 'sr-tr,serbian', 'su-arab,sundanese (arabic)', 'su-java,sundanese', 'su-sund,sundanese', 'sv-brai,swedish (braille)', 'sv-runr,swedish (runic)', 'sv-se,swedish (sweden)', 'sw-arab,swahili (arabic)', 'sw-arab-cd,swahili (arabic congo kinshasa)', 'sw-brai,swahili (braille)', 'sw-tz,swahili (tanzania)', 'ta-arab,tamil (arabic)', 'ta-brai,tamil (braille)', 'ta-in,tamil (india)', 'te-brai,telugu (braille)', 'te-in,telugu (india)', 'teo-ug,teso (uganda)', 'tg-arab,tajik (arabic)', 'tg-hebr,tajik', 'tg-latn,tajik', 'tg-tj,tajik (tajikistan)', 'th-brai,thai (braille)', 'th-th,thai (thailand)', 'ti-arab,tigrinya (arabic)', 'ti-et,tigrinya (ethiopia)', 'tk-arab,turkmen (arabic)', 'tk-arab-ir,turkmen (arabic iran)', 'tk-cyrl,turkmen (cyrillic)', 'to-to,tongan (tonga)', 'tr-arab,turkish (arabic)', 'tr-brai,turkish (braille)', 'tr-cyrl,turkish (cyrillic)', 'tr-grek,turkish', 'tt-arab,tatar (arabic)', 'tt-latn,tatar (latin)', 'tt-ru,tatar (russia)', 'twq-ne,tasawaq (niger)', 'tzm-arab,central atlas tamazight (arabic)', 'tzm-ma,central atlas tamazight (morocco)', 'tzm-tfng,central atlas tamazight (tifinagh)', 'ug-cn,uighur (china)', 'ug-kz,uyghur', 'ug-latn,uighur (latin)', 'ug-mn,uighur', 'uk-latn,ukrainian (latin)', 'uk-ua,ukrainian (ukraine)', 'ur-brai,urdu (braille)', 'ur-deva,urdu (devanagari)', 'ur-latn,urdu', 'ur-pk,urdu (pakistan)', 'uz-arab-af,uzbek (arabic afghanistan)', 'uz-brai,uzbek (braille)', 'uz-cn,uzbek (china)', 'uz-cryl,uzbek (cyrillic)', 'uz-latn,uzbek (latin)', 'uz-latn-uz,uzbek (latin uzbekistan)', 'uz-sogd,uzbek', 'vai-latn-lr,vai (latin liberia)', 'vai-vaii,vai (vai)', 'vai-vaii-lr,vai (vai liberia)', 'vi-brai,vietnamese (braille)', 'vi-hani,vietnamese', 'vi-vn,vietnamese (vietnam)', 'vi-x-central,vietnamese', 'vi-x-ncentral,vietnamese', 'vi-x-southern,vietnamese', 'vi-x-sub25,vietnamese', 'vi-x-sub31,vietnamese', 'vi-x-sub39,vietnamese', 'vi-x-sub49,vietnamese', 'vi-x-sub67,vietnamese', 'vi-x-subdn,vietnamese', 'vun-tz,vunjo (tanzania)', 'wae-ch,walser (switzerland)', 'wo-arab,wolof (arabic)', 'wo-sn,wolof (senegal)', 'xh-brai,xhosa (braille)', 'xog-ug,soga (uganda)', 'yav-cm,yangben (cameroon)', 'yi-001,yiddish (world)', 'yo-arab,yoruba (arabic)', 'yo-brai,yoruba (braille)', 'yo-ng,yoruba (nigeria)', 'yue-brai,cantonese (braille)', 'yue-hant,cantonese (traditional)', 'yue-hant-hk,cantonese (traditional hong kong)', 'yue-latn,yue (latin)', 'zgh-ma,standard moroccan tamazight (morocco)', 'zh-arab,chinese (arabic)', 'zh-au,chinese', 'zh-bn,chinese', 'zh-bopo,chinese', 'zh-brai,mandarin (braille)', 'zh-gb,chinese (united kingdom)', 'zh-gf,chinese', 'zh-hanb,chinese', 'zh-hans-cn,chinese (simplified china)', 'zh-hans-sg,chinese (simplified singapore)', 'zh-hant-hk,chinese (traditional hong kong)', 'zh-hant-tw,chinese (traditional taiwan)', 'zh-id,chinese (indonesia)', 'zh-latn,chinese (latin)', 'zh-mo,chinese (macau)', 'zh-pa,chinese (panama)', 'zh-pf,chinese', 'zh-ph,chinese (philippines)', 'zh-phag,chinese', 'zh-sr,chinese', 'zh-th,chinese', 'zh-us,chinese', 'zh-vn,chinese (vietnam)', 'zu-brai,zulu (braille)', 'zu-za,zulu (south africa)', //*/ // gecko: supportedlocaleof, running maxmimum // 779 + 1093: with these in // 777 + 1083: with these no included // get names and sort into place in expanded // they don't add entropy in locale POCs 'gaa-arab', 'kxv-latn', 'nds-nl', 'nds-runr', 'nso-brai', 'uz-cyrl', 'vmw-arab', 'xnr-takr', 'za-hani', 'za-hans', ] let gLocalesCanonical = [ //* // redundant: see getCanonicalLocale 'arb-eg,arabic standard (egypt)', // ar-eg 'arb-lb,arabic standard (lebanon)', // ar-lb 'cnr,montenegrin', // sr-me 'cnr-latn,montenegrin', // sr-latn-me 'gom,goan', // kok 'gom-knda', // kok-knda 'gom-latn', // kok-latn 'prp,parsi', // gu 'prs,dari', // fa-af 'sh,serbo-croatian', // sr-latn 'swc,congo', // sw-cd 'swc-arab,congo (arabic)', // sw-arab-cd 'swh-arab,swahili (arabic)', // sw-arab 'swh-brai,swahili (braille)',// sw-brai 'tl,tagalog', // fil 'tl-brai,tagalog (braille)', // fil-brai 'tl-buhd,tagalog', // fil-buhd 'tl-hano,tagalog', // fil-hano 'tl-tglg,tagalog (tagalog)', // fil-tglg 'tw,twi', // ak 'tw-x-asante,twi', // ak-x-asante //*/ ] function check_canonicals() { let list = gLocales list = list.concat(gLocalesLikely) list = list.concat(gLocalesExpand) list = list.concat(gLocalesCanonical) list = list.filter(function(item, position) {return list.indexOf(item) === position}) let aCanonical = [] list.forEach(function(item) { let code = item.split(',')[0] let test = Intl.getCanonicalLocales([code]) if (test.join(',').toLowerCase() !== code.toLowerCase()) { aCanonical.push(code +': ' + test.join(', ')) } }) if (aCanonical.length) { console.log('canonicals detected\n-------\n', aCanonical.join('\n ')) } } function expand_likely() { gLocales = gLocales.concat(gLocalesLikely) gLocales = gLocales.filter(function(item, position) {return gLocales.indexOf(item) === position}) } function expand_maximum() { gLocales = gLocales.concat(gLocalesLikely) gLocales = gLocales.concat(gLocalesExpand) gLocales = gLocales.filter(function(item, position) {return gLocales.indexOf(item) === position}) } // expand once in a while to see if entropy counts change //expand_likely() //expand_maximum() // also checks for canonical names ================================================ FILE: tests/testindex.css ================================================ :root{ /* sat 73 */ --test0: #b3b3b3; --test1: #dc9d9d; --test2: #dcb29d; --test3: #dcc79d; --test4: #dcdc9d; --test5: #c7dc9d; --test6: #b2dc9d; --test7: #9ddc9d; --test8: #9ddcb2; --test9: #9ddcc7; --test10: #9ddcdc; --test11: #9dc7dc; --test12: #9db2dc; --test13: #9d9ddc; --test14: #b29ddc; --test15: #c79ddc; --test16: #dc9ddc; --test17: #dc9dc7; --test18: #dc9db2; --test99: #808080; --testbad: #ff8787; /* sat 120 */ /* sat 90 */ --bg0: #161b22; --bg1: #f09b9b; --bg2: #f0b89b; --bg3: #f0d49b; --bg4: #f0f09b; --bg5: #d4f09b; --bg6: #b8f09b; --bg7: #9bf09b; --bg8: #9bf0b8; --bg9: #9bf0d4; --bg10: #9bf0f0; --bg11: #9bd4f0; --bg12: #9bb8f0; --bg13: #9b9bf0; --bg14: #b89bf0; --bg15: #d49bf0; --bg16: #f09bf0; --bg17: #f09bd4; --bg18: #f09bb8; --bg99: #808080; --jstring: #ff7de9; --jboolean: #86de74; --jnull: #939395; --jkey: #75bfff; } #modaloverlay { position: fixed; top: 0px; left: 0px; width: 100%; height: 100%; z-index: 990; display: none; } #overlay { position: fixed; top: 50%; left: 50%; right: 0; bottom: 0; transform: translate(-50%, -50%); display: none; width: 700px; min-width: 400px; height: 85%; overflow-y:scroll; border: 2px solid var(--test0); background-color: var(--bg0); z-index: 1000; overscroll-behavior: contain; } #overlaybuttons { position: fixed; right: 25px; z-index: 1010; } /* JSON */ .string {color: var(--jstring);} .boolean, .number {color: var(--jboolean);} .null {color: var(--jnull);} .key {color: var(--jkey);} body {background-color: #161b22; color: var(--test0);} h2 {color: white; font-size: 14px; text-align: center; margin-top: inherit;} code { background: rgba(142, 142, 145, 0.25) !important; padding: 2px 6px; /* top+bottom | left+ right */ } a {color: black; text-decoration: none;} a.blue {color: var(--test12); text-decoration: none;} a.return {color: var(--test12); text-decoration: none; font-size: 14px; line-height: 1.2em} .no_color {color: var(--test0);} .good {color: var(--test7);} .bad {color: var(--testbad);} .blue {color: var(--test12);} .faint {color: var(--test99);} .hidden {display: none;} .small {font-size: 10px;} .offscreen { position: absolute !important; top: -2000% !important; left: 0px !important; } .indent { display:inline-block; margin-left:20px; } .bold {font-weight: bold;} .mono {font-family: monospace, "Courier New"; font-size: 12px;} .strike {text-decoration: line-through;} .spaces {white-space: pre-wrap;} .perf {font-family: monospace, "Courier New"; font-size: 12px; white-space: pre-wrap;} .cursive {font-family: cursive;} .emoji {font-family: emoji;} .fangsong {font-family: fangsong;} .fantasy {font-family: fantasy;} .math {font-family: math;} .monospace {font-family: monospace;} .none {font-family: none;} .sans-serif {font-family: sans-serif;} .serif {font-family: serif;} .system-ui {font-family: system-ui;} .ui-monospace {font-family: ui-monospace;} .ui-rounded {font-family: ui-rounded;} .ui-sans-serif {font-family: ui-sans-serif;} .ui-serif {font-family: ui-serif;} .normalized { font-family: none !important; font-size: initial !important; /* testing */ font-style: normal !important; font-variant: normal !important; font-weight: normal !important; line-height: normal !important; text-transform: none !important; text-align: left !important; text-decoration: none !important; text-shadow: none !important; white-space: normal !important; word-break: normal !important; word-spacing: normal !important; } /* run/re-run, click here */ .btn {background-color: #161b22; display: inline-block; font-size: 12px; font-family: monospace, "Courier New"; font-weight: bold; padding-left: 6px; padding-right: 6px; cursor: pointer; } .btnfirst {background-color: #161b22; display: inline-block; font-size: 12px; font-family: monospace, "Courier New"; font-weight: bold; padding-left: 0px; padding-right: 6px; cursor: pointer; } .btnb {cursor: pointer;} /* item metrics/counts: dotted, no padding, normal */ .btnc { font-weight: normal; text-decoration: underline; text-decoration-style: dotted; cursor: pointer; } /* section metrics/counts: underline, padded, bold */ .btns { font-weight: bold; text-decoration: underline; padding-left: 8px; padding-right: 8px; cursor: pointer; } .btn0 {border-color: var(--test0); color: var(--test0);} .btn1, .s1 {border-color: var(--test1); color: var(--test1);} .btn2, .s2 {border-color: var(--test2); color: var(--test2);} .btn3, .s3 {border-color: var(--test3); color: var(--test3);} .btn4, .s4 {border-color: var(--test4); color: var(--test4);} .btn5, .s5 {border-color: var(--test5); color: var(--test5);} .btn6, .s6 {border-color: var(--test6); color: var(--test6);} .btn7, .s7 {border-color: var(--test7); color: var(--test7);} .btn8, .s8 {border-color: var(--test8); color: var(--test8);} .btn9, .s9 {border-color: var(--test9); color: var(--test9);} .btn10, .s10 {border-color: var(--test10); color: var(--test10);} .btn11, .s11 {border-color: var(--test11); color: var(--test11);} .btn12, .s12 {border-color: var(--test12); color: var(--test12);} .btn13, .s13 {border-color: var(--test13); color: var(--test13);} .btn14, .s14 {border-color: var(--test14); color: var(--test14);} .btn15, .s15 {border-color: var(--test15); color: var(--test15);} .btn16, .s16 {border-color: var(--test16); color: var(--test16);} .btn17, .s17 {border-color: var(--test17); color: var(--test17);} .btn18, .s18 {border-color: var(--test18); color: var(--test18);} .s99 {color: var(--test99);} .btnbad, .sbad {border-color: var(--testbad); color: var(--testbad);} .btn-left {float: left; position: relative; left: -10px; top: 0px;} .btn-right {float: right; position: relative; right: 10px; top: 0px; text-align: right;} /* tooltips */ .icon {font-size: 10px; font-weight: bold; color: var(--test0); cursor: default;} .ttip {position: relative; display: inline-block; border-bottom: 1px dotted black; color: white;} .ttip .ttxt { visibility: hidden; width: 150px; background-color: var(--test0); color: black; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -12px; left: 130%;} .ttip .ttxtb { visibility: hidden; width: 210px; background-color: var(--test0); color: black; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -12px; left: 130%;} .ttip .ttxtx { visibility: hidden; width: 250px; background-color: var(--test0); color: black; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; top: -12px; left: 130%;} .ttip:hover .ttxt, .ttip:hover .ttxtb, .ttip:hover .ttxtx {visibility: visible;} .ttip .ttxt::after, .ttip .ttxtb::after, .ttip .ttxtx::after { content: " "; position: absolute; top: 50%; right: 100%; margin-top: -5px; border-width: 5px; border-style: solid; border-color: transparent var(--test0) transparent transparent;} /* table nav */ div.nav-title {position: relative;} div.nav-down {position: absolute; right: 20px; top: 0px; width: 100px; text-align: right;} div.nav-up {position: absolute; left: 20px; top: 0px; width: 100px; text-align: left;} /* tables */ table { border-collapse: collapse; margin: 0 auto 10px auto; font-size: 12px; } tbody:before { content: "-"; /* this line affects table tests in elements_other but we add an elements-fp rule */ display: block; line-height: 1em; color: transparent; } td {padding-top: 2px; padding-bottom: 3px;} th {color: black; font-size: 14px; padding: 3px 0;} table td:first-child { text-align: right; vertical-align: top;} table td.blurb {text-align: center; line-height: 1.5em;} table td.center {text-align: center;} table td.intro {text-align: left; line-height: 1.5em; padding-bottom: 10px;} table td.showhide {text-align: center; padding-top: 7px; padding-bottom: 7px;} table td.padr {padding-right: 10px;} table td.bottom {vertical-align: bottom;} #tb1 th {background-color: var(--bg1);} #tb2 th {background-color: var(--bg2);} #tb3 th {background-color: var(--bg3);} #tb4 th {background-color: var(--bg4);} #tb5 th {background-color: var(--bg5);} #tb6 th {background-color: var(--bg6);} #tb7 th {background-color: var(--bg7);} #tb8 th {background-color: var(--bg8);} #tb9 th {background-color: var(--bg9);} #tb10 th {background-color: var(--bg10);} #tb11 th {background-color: var(--bg11);} #tb12 th {background-color: var(--bg12);} #tb13 th {background-color: var(--bg13);} #tb14 th {background-color: var(--bg14);} #tb15 th {background-color: var(--bg15);} #tb16 th {background-color: var(--bg16);} #tb17 th {background-color: var(--bg17);} #tb18 th {background-color: var(--bg18);} #tb99 th {background-color: var(--bg99);} #tb1 td:first-child {color: var(--test1);} #tb2 td:first-child {color: var(--test2);} #tb3 td:first-child {color: var(--test3);} #tb4 td:first-child {color: var(--test4);} #tb5 td:first-child {color: var(--test5);} #tb6 td:first-child {color: var(--test6);} #tb7 td:first-child {color: var(--test7);} #tb8 td:first-child {color: var(--test8);} #tb9 td:first-child {color: var(--test9);} #tb10 td:first-child {color: var(--test10);} #tb11 td:first-child {color: var(--test11);} #tb12 td:first-child {color: var(--test12);} #tb13 td:first-child {color: var(--test13);} #tb14 td:first-child {color: var(--test14);} #tb15 td:first-child {color: var(--test15);} #tb16 td:first-child {color: var(--test16);} #tb17 td:first-child {color: var(--test17);} #tb18 td:first-child {color: var(--test18);} #tb99 td:first-child {color: var(--test99);} #tb1 hr {color: var(--test1);} #tb2 hr {color: var(--test2);} #tb3 hr {color: var(--test3);} #tb4 hr {color: var(--test4);} #tb5 hr {color: var(--test5);} #tb6 hr {color: var(--test6);} #tb7 hr {color: var(--test7);} #tb8 hr {color: var(--test8);} #tb9 hr {color: var(--test9);} #tb10 hr {color: var(--test10);} #tb11 hr {color: var(--test11);} #tb12 hr {color: var(--test12);} #tb13 hr {color: var(--test13);} #tb14 hr {color: var(--test14);} #tb15 hr {color: var(--test15);} #tb16 hr {color: var(--test16);} #tb17 hr {color: var(--test17);} #tb18 hr {color: var(--test18);} #element-fp { position: fixed; top: 0; left: 0; font-family: none; font-size: initial; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; text-transform: none; text-align: left; text-decoration: none; text-shadow: none; white-space: nowrap; /* domrect */ /*transform: scale(1.1234567);*/ transform: skew(1.787542deg,3.263901deg); } #element-fp .unstyled { -moz-appearance: none; -webkit-appearance: none; appearance: none; } #element-fp tbody:before { content: none; line-height: normal; } #element-fp .revert { all: revert; } .skew { transform: skew(1.787542deg,3.263901deg); } ================================================ FILE: tests/timezones.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=600"> <title>timezones</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 97%; min-width: 580px; max-width: 780px;} div.pad {pad-bottom: 10px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#region">return to TZP index</a></td></tr> </table> <table id="tb4"> <col width="25%"><col width="75%"> <thead><tr><th colspan="2"> <div class="nav-title">timezones <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="2" class="intro"> <span class="no_color">A proof that calculates the maximum outcomes in timezone offsets between timezones. Testing with an empty input runs the example. Reducing the number of years loses maximum outcomes. Adding more years gains nothing. You can, however, swap some years for others. </span> </td></tr> <tr> <td class="mono spaces" id="legend" style="text-align: left; vertical-align: top; color: #b3b3b3; font-size: 11px;"></td> <td class="mono" style="text-align: left; vertical-align: top;"> <span class="btn4 btnfirst" onClick="run('run')">[ run ]</span><span class="btn4 btn" onClick="run('summary')">[ summary ]</span> <select name="measure" id="measure"> <option value="math">date.parse</option> <option value="tzn">timeZoneName</option> <option value="both">both</option> </select> <span id="elementSupported"><input type="checkbox" id="useAll"> all</span> | <span class="btn4 btn" onClick="run('tzn')">[ tzn ]</span> <br><br> <label style="display: flex;"> <input type="text" style="flex: 1;" placeholder="comma delimited year(s)" name="inputYears" id="inputYears"> <span class="btn4 btn" onClick="reset()"> [clear]</span> </label> <br> <span><hr></span> <br> <span class="spaces" id="info" style="line-height: 1.5em;"></span> <br> <span class="spaces" id="data"></span> </td></tr> </table> <br> <script> 'use strict'; var aLegend = [], aLegendFull = [], oLists = {}, aDates = [], aDays = ["January 15","July 15"], // to match TZP min test // to make sure we don't change years or months when a day or two ticks over // use the 15th - this makes get* and getUTC* PoCs possible aYears = [], aYearsTZP = [], useSupported, isLog = false let oData = {}, oDataShort = {}, oBucketed = {}, oUndefined = {}, aBuckets = [], aShortNames = [], loopData = [] function build_dates(year) { // reset aDates = [] let suffix = "13:00:00 UTC" if (year !== undefined && year !== "") { // single year aYears = [year] } else { // aYears is set in run() // sort numerically & dedupe aYears.sort((b,a) => b-a) aYears = aYears.filter(function(item, position) {return aYears.indexOf(item) === position}) } // build for (let i = 0; i < aYears.length; i++) { for (let j = 0; j < aDays.length; j++) { aDates.push(aDays[j] +", "+ aYears[i] +" "+ suffix ) } } } function log(item) { try { item = item.toUpperCase() if ('GROUPS' == item) { console.log(item, aBuckets.length, mini(aBuckets), '\n', aBuckets) } else if ('RESULTS' == item) { console.log(item, mini(oBucketed) +'\n', oBucketed) } else if ('FINGERPRINT' == item) { console.log(item, mini(oUndefined) +'\n', oUndefined) } else if ('TIMEZONENAMES' == item) { console.log(item +' DATA', '\n', oDataShort) console.log(item +' LIST', aShortNames.length, '\n', aShortNames) } } catch(e) { console.log(e) } } function get_lengths() { // display string lengths for tzp let max = 0 let oLengths = {} for (const k of Object.keys(oData)) { let display = [] for (const y of Object.keys(oData[k].data)) { display.push(oData[k].data[y].join(', ')) } let str = display.join(' | ') let len = str.length if (len > max) { max = len //console.log(max, str) } if (undefined == oLengths[len]) { oLengths[len] = 1 } else { oLengths[len] = oLengths[len] + 1 } } console.log(max) console.log(oLengths) } function reset() { dom.inputYears.value = "" } function legend() { // pick our list let proceed = false, list = [] if (useSupported) { if (aLegend.length == 0) {proceed = true} list = oLists.supported } else { if (aLegendFull.length == 0) {proceed = true} list = oLists.full } let prev = '' // do once if (proceed) { list.sort() for (let i = 0 ; i < list.length; i++) { let parts = list[i].split('/') let area = parts[0] let location = parts.slice(1).join('/') if (area == prev) { if (useSupported) {aLegend.push(' '+ location)} else {aLegendFull.push(' '+ location)} } else { let str = '<br>'+ s4+ (area == 'AA' ? 'MISC' : area.toUpperCase()) + sc +'<br><br> '+ location if (useSupported) {aLegend.push(str)} else {aLegendFull.push(str)} } prev = area } } let aDisplay = useSupported ? aLegend : aLegendFull let header = s4.trim() +"TIMEZONES ["+ list.length + (useSupported ? " supported" : "") +"] "+ sc +"<br><br>" +"<a class='blue' target='blank' href='https://www.iana.org/time-zones'>IANA TimeZone Database</a><br>" dom.legend.innerHTML = header + aDisplay.join("<br>") } function run_test(type) { let t0 = performance.now() let method = dom.measure.value let methodStr = 'math' == method ? 'date.parse' : ('tzn' == method ? 'timeZoneName' : 'date.parse + timeZoneName') let yrStr = aYears.length > 10 ? aYears.slice(0,3).join(', ') +' ... and ' + (aYears.length - 3) +' more' : aYears.join(', ') let isTZP = 'math' == method && (aYears.join(',') == aYearsTZP.join(',')) let padSize = 15 function title(input, color) { if (color == undefined) {return input.padStart(padSize) +': '} else {return color + input.padStart(padSize) +': '+ sc} } function test() { // reset oData = {} oDataShort = {} oBucketed = {} aBuckets = [] aShortNames = [] // vars let tmpoption = { day: '2-digit', month: '2-digit', year: 'numeric', hour12: false, hour: '2-digit', minute: 'numeric', second: 'numeric', timeZoneName: 'short', } let k = 60000, tzresults = {}, uniqueTZs = 0, undefinedError, aDisplay = [], aInvalid = [], aBucketSizes = [] if ('run' == type) { // get undefined hash /* notes: because we are formatting (toLocaleString or DTF etc) to get the timezonename we need to ensure both use the same format (en) otherwise the string can mix e.g. 1/7 vs 7/1 in TZP we don't format and if we do it will only be for a timeZoneName PoC */ oUndefined = {} tmpoption.timeZone = undefined try { for (let i = 0 ; i < aYears.length; i++) { let year = aYears[i], control, test oUndefined[year] = [] for (let j = 0 ; j < aDays.length; j++) { let datetime = aDays[j] +', '+ aYears[i] +' 13:00:00' let control = new Date(datetime) let test = control.toLocaleString('en', {timeZone: 'UTC'}) let formatter = new Intl.DateTimeFormat('en', tmpoption) control = formatter.format(control).replace(',','') let tznshort = control.split(' ')[2] control = control.slice(0,19) // remove tzn let diff = ((Date.parse(test) - Date.parse(control))/k) if ('math' == method || 'both' == method) {oUndefined[year].push(diff)} if ('tzn' == method || 'both' == method) {oUndefined[year].push(tznshort)} } // dedupe year data oUndefined[year] = oUndefined[year].filter(function(item, position) {return oUndefined[year].indexOf(item) === position}) } } catch(e) { undefinedError = e+'' } } // select list and loop each timezone let array = useSupported ? oLists["supported"] : oLists["full"] // get data let tmpData = {} for (let i = 0 ; i < array.length; i++) { let tz = array[i] let tzclean = tz // strip bogus area if (tz.substring(0,3) == "AA/") { tzclean = tz.substring(3)} let tzvalid = 'UTC' == tzclean ? 'GMT' : tzclean tmpoption.timeZone = tzvalid tzresults = {} // loop each date try { for (let i = 0 ; i < aYears.length; i++) { let year = aYears[i] tzresults[year] = [] for (let j = 0 ; j < aDays.length; j++) { let datetime = aDays[j] +", "+ aYears[i] +" 13:00:00" let control = new Date(datetime) let test = control.toLocaleString("en", {timeZone: "UTC"}) let formatter = new Intl.DateTimeFormat('en', tmpoption) control = formatter.format(control).replace(',','') let tznshort = control.split(' ')[2] control = control.slice(0,19) let diff = ((Date.parse(test) - Date.parse(control))/k) if ('math' == method || 'both' == method) {tzresults[year].push(diff)} if ('tzn' == method || 'both' == method) {tzresults[year].push(tznshort)} if ('run' == type || 'tzn' == type) { if (undefined == oDataShort[tznshort]) { oDataShort[tznshort] = [tzclean] } else { oDataShort[tznshort].push(tzclean) } } /* what timezones in have part hours if (2026 == aYears[i]) { let partial = diff % 60 if (partial !== 0) {console.log(aYears[i], tz, diff)} } //*/ } // dedupe year results tzresults[year] = tzresults[year].filter(function(item, position) {return tzresults[year].indexOf(item) === position}) tmpData[tzclean] = { 'hash': mini(tzresults), 'data': tzresults } } } catch(e) { aInvalid.push(tzclean) } } // dedupe short timezonenames collection if ('tzn' == type || 'run' == type) { // map names into an order let tznShort = { AKST: 540, AKDT: 480, AST: 240, ADT: 180, CST: 360, CDT: 300, EST: 300, EDT: 240, HAST: 600, HADT: 540, HST: 600, HDT: 540, MST: 420, MDT: 360, PST: 480, PDT: 420, UTC: 0, GMT: 0, 'GMT+0': 0, } let oOrder = {} let oMinutes = {} for (const k of Object.keys(oDataShort)) { // dedupe and sort let parts let tzones = oDataShort[k] tzones = tzones.filter(function(item, position) {return tzones.indexOf(item) === position}) tzones.sort() oDataShort[k] = tzones // calculate numerical order let minutes = 'TBA' if (undefined !== tznShort[k]) { minutes = tznShort[k] } else { // should be just left with GMTs let value = k let sign = value.includes('-') ? 1 : -1 value = value.replace('GMT','') value = value.replace('-','') value = value.replace('+','') let parts = value.split(':') minutes = parts[0] * 60 if (undefined !== parts[1]) { minutes += parts[1] * 1} // minutes if (undefined !== parts[2]) { minutes += (parts[2] * 1)/60} // seconds minutes = minutes * sign } oMinutes[k] = minutes if (undefined == oOrder[minutes]) {oOrder[minutes] = {}} if (undefined == oOrder[minutes][k]) {oOrder[minutes][k] = oDataShort[k].sort()} } //console.log(oOrder) //console.log(oMinutes) // populate aShortNames for (const m of Object.keys(oOrder).sort((a,b) => b-a)) { for (const k of Object.keys(oOrder[m]).sort()) {aShortNames.push(k)} } if ('tzn' == type) { aDisplay.push(s12 +'YEARS: '+ sc + yrStr +'<br>') aDisplay.push(s12 +'TIMEZONENAMES: '+ sc + s4 +' ['+ aShortNames.length +']' + sc +'<br><br>'+ aShortNames.join(', ') +'<br>') aDisplay.push(s12 +'DATA: '+ sc +'<br>') aShortNames.forEach(function(k) { aDisplay.push(s14 + k + sc +': '+ s4 +'['+ oDataShort[k].length +'] '+ sc + s7 + oMinutes[k] + sc) aDisplay.push('<li>' + oDataShort[k].join(', ') +'</li>') }) dom.info.innerHTML = aDisplay.join('<br>') +'<br>' dom.perf.innerHTML = Math.round(performance.now() - t0) +' ms' return } } // structure/tidy our data for (const k of Object.keys(tmpData).sort()) { let tmphash = tmpData[k].hash if (undefined == oData[tmphash]) { oData[tmphash] = {'data': tmpData[k]['data'],'timezones': [k]} } else { oData[tmphash]['timezones'].push(k) } } for (const k of Object.keys(oData)) { let count = oData[k].timezones.length oData[k]['count'] = count aBuckets.push(oData[k]['timezones'].join(', ')) aBucketSizes.push(count) if (undefined == oBucketed[count]) {oBucketed[count] = {}} // group by count oBucketed[count][k] = {'data': oData[k]['data'], 'timezones': oData[k]['timezones']} } if (undefined !== oBucketed['1']) {uniqueTZs = Object.keys(oBucketed['1']).length} aBuckets.sort() aInvalid = aInvalid.filter(function(item, position) {return aInvalid.indexOf(item) === position}) aInvalid.sort() let buckethash = mini(aBuckets) aBucketSizes.sort((b,a) => b-a) let sumTZ = aBucketSizes.reduce(function(a, b){return a + b}, 0) if (isLog && 'run' == type) { console.log('tmpData\n',tmpData) console.log('oData HASH/DATA', Object.keys(oData).length, mini(oData) +'\n', oData) console.log('BUCKETSIZES', '[sum of sizes:', sumTZ, ']\n', aBucketSizes) } if ('summary' == type) { loopData.push(s12 + loopYear + sc +' '+ buckethash + s4 +' ['+ aBucketSizes.length +'] '+ sc + sg +'['+ uniqueTZs +']'+ sc) dom.perf.innerHTML = Math.round(performance.now() - t0) +' ms' return } // display data aDisplay = ['<hr>'] let counter = 0 for (const c of Object.keys(oBucketed).sort((a,b) => b-a)) { // count let isOnes = '1' == c if (isOnes) { let total = Object.keys(oData).length, next = counter + 1, remainder = total - counter aDisplay.push('<hr>timezones in '+ s12 + 'hashes '+ next + '-' + total + sc +' are all unqiue'+ s4 +' ['+ remainder +']'+ sc +'<br>' ) } for (const h of Object.keys(oBucketed[c])) { // hash counter++ let str = '' if (isOnes) { str = s12 + h + sc + ' ' + oBucketed[c][h].timezones + '<ul>' } else { str = s12 + (counter+'').padStart(3,' ') +': '+ h + sc + s4 +' ['+ c +']' + sc str += '<ul><li>' + oBucketed[c][h].timezones.join(', ') +'</li>' } let oTmp = oBucketed[c][h].data, dTmp = [] for (const y of Object.keys(oTmp)) {dTmp.push(s4 + y +sc +': ' + oTmp[y].join(', '))} str += '<li>'+ dTmp.join('<br>') +'</li></ul>' aDisplay.push(str) } } dom.data.innerHTML = aDisplay.join('<br>') // display info aDisplay = [] function splitgroup(strToSplit) { let parts = strToSplit.split(", "); parts = parts.join('<br>' + ('').padStart(padSize + 2)); return parts } aDisplay.push( title('YEARS', s14) + yrStr) aDisplay.push( title('METHOD', s14) + methodStr) aDisplay.push( title('TIMEZONES', s14) + (useSupported ? 'supported only' : 'all') +'<br>') aDisplay.push( // color it up title('unique') + sg + (aBucketSizes.length - uniqueTZs).toString().padStart(3) + sc +' groups (>1) ' + s4 + (array.length - aInvalid.length - uniqueTZs).toString().padStart(3) + sc + ' timezones<br>' + title('') + sg + (uniqueTZs).toString().padStart(3) + sc +' groups (=1) ' + s4 + (uniqueTZs).toString().padStart(3) + sc + ' timezones<br>' + title('total') + sg + (aBucketSizes.length).toString().padStart(3) + sc +' groups .... ' + s4 + (array.length - aInvalid.length).toString().padStart(3) + sc + ' timezones' ) aDisplay.push( title('[groups] hash') + '<span class="s12 btnc" onClick="log(' + "'groups'"+ ')">' + buckethash + '</span>') aDisplay.push( title('[results] hash') + '<span class="s12 btnc" onClick="log(' + "'results'"+ ')">' + mini(oBucketed) + '</span>') aDisplay.push( title('timezonenames') + '<span class="s12 btnc" onClick="log(' + "'timezonenames'"+ ')">' + 'details' + '</span>') let fpstring = title((isTZP ? '[tzp]' : '[your]') + ' hash') if (undefined == undefinedError) { aDisplay.push( fpstring + '<span class="s16 btnc" onClick="log('+ "'fingerprint'"+ ')">' + mini(oUndefined) + '</span>') } else { aDisplay.push( fpstring + undefinedError) } // invalid timezones if (sumTZ !== array.length) { aDisplay.push( title('alert', sb) +'total timezones '+ sb +'(' + sumTZ +')'+ sc +' !== legend count '+ sb + '(' + array.length +')'+ sc) } if (aInvalid.length) { aDisplay.push( title(aInvalid.length +' invalid', sb) + aInvalid[0]) if (aInvalid.length > 1) {aDisplay.push(title('') + splitgroup((aInvalid.slice(1)).join(', ')))} } // group info aBucketSizes.sort((a,b) => b-a) let aBucketSizesUnique = aBucketSizes.filter(function(item, position) {return aBucketSizes.indexOf(item) === position}) let notoneBS = aBucketSizes.filter(x => x != 1) aDisplay.push(s4 +'<br>unique groups sizes'+ sc +'<br>'+ aBucketSizesUnique.join(', ') +'<br>') aDisplay.push(s4 +'groups > 1 summary'+ sc +'<br>'+ notoneBS.join(', ')) dom.info.innerHTML = aDisplay.join('<br>') +'<br>' dom.perf.innerHTML = Math.round(performance.now() - t0) +" ms" return } let loopYear = '' if ('summary' == type) { // reset loopData loopData = [] loopData.push( title('METHOD', s14) + methodStr) loopData.push( title('TIMEZONES', s14) + (useSupported ? 'supported only' : 'all') +'<br>') loopData.push(s12 +'year'+ sc +' hash-of-timezone-groups' + s4 +' [groups] '+ sc + sg +'[unique timezones]'+ sc +'<br>') // loop let loopYears = aYears for (let i = 0; i < loopYears.length; i++) { loopYear = loopYears[i] build_dates(loopYears[i]) test() } // output loopData dom.info.innerHTML = loopData.join('<br>') dom.perf.innerHTML = Math.round(performance.now() - t0) +' ms' return } else { test() } } function run(type) { useSupported = !dom.useAll.checked let go = false // reset legend legend() dom.info = '' dom.data = '' dom.perf = '' aYears = [] let yearStr = dom.inputYears.value yearStr = yearStr.trim() if (yearStr.length) { // parse for valid years let tmpArr = yearStr.split(',') for (let i = 0 ; i < tmpArr.length; i++) { let trimmed = tmpArr[i].trim() if (trimmed.length) { // only collect numbers trimmed = parseInt(trimmed) if (Number.isInteger(trimmed)) { // put a range limit on it if (trimmed > -2000 && trimmed < 3000) { aYears.push(trimmed) } } } } // do we have at least one year if (aYears.length) { go = true } else { dom.info = 'please provide at least one year' } } else { // run examples // anything prior to 1879 is redundant // tested: every year from 0-2100 // tested: negative years // use a year in the future to catch some changes from tzdata aYears = aYearsTZP /* old: using 2018 unique: 49 groups (>1) 173 timezones : 296 groups (=1) 296 timezones total: 345 groups .... 469 timezones new: using 2025 unique: 48 groups (>1) 171 timezones <- less shared : 298 groups (=1) 298 timezones <- more unique total: 346 groups .... 469 timezones <- more hashes kaching! matches */ // test flips //aDays = ["January 1","April 1","July 1","October 1"] // do four days: no extra entropy //for (let i = 1879; i < 2030; i++) {aYears.push(i)} // 1879+ //for (let i = 1800; i < 1900; i++) {aYears.push(i)} // add a century //for (let i = -1050; i < -350; i=i+7) {aYears.push(i)} // add negative years //for (let i = 2000; i < 2100; i++) {aYears.push(i)} // add the future dom.inputYears.value = aYears.join(',') go = true } if (go) { dom.info = 'calculating...' // rebuild aDates build_dates() // pause for repaint function test() { clearInterval(checking) run_test(type) } let checking = setInterval(test, 10) } } function start() { get_globals() get_isVer() // chrome has more groups with full vs supported // but this creates a false sense of uniqueness as aliases pair up // e.g. supported = 280 | all = 229 //dom.useAll.checked = isFF ? false : true dom.useAll.checked = false aYearsTZP = [1879,1952,1976,2025] if (isVer < 112) {aYearsTZP = [1879,1921,1952,1976,2025]} /* aYearsTZP = [] for (let i = 1879; i < 2030; i++) { aYearsTZP.push(i) } */ // remove any sloppy duplicates gTimezones = gTimezones.filter(function(item, position) {return gTimezones.indexOf(item) === position}) // add AA let baselist = [] gTimezones.forEach(function(item){ if (!item.includes('/')) {item = 'AA/'+ item} baselist.push(item) }) baselist.sort() oLists['full'] = baselist try { let supported = Intl.supportedValuesOf('timeZone') let newlist = [] supported.forEach(function(item){ if (!item.includes('/')) { item = 'AA/'+ item} newlist.push(item) }) newlist.sort() oLists['supported'] = newlist } catch(e) { dom.useAll.checked = true dom.elementSupported.style.display = 'none' } legend() } start() </script> </body> </html> ================================================ FILE: tests/versions.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=700"> <title>versions</title> <link rel="stylesheet" type="text/css" href="testindex.css"> <script src="testglobals.js"></script> <script src="testgeneric.js"></script> <style> table {width: 680px;} </style> <style id="test73"> .a{border-width: 1px; border-style: solid; border-color: black; border-image: linear-gradient(white,black);} li {margin-left: -20px;} </style> </head> <body> <div class="hidden"> <div id="test73"></div> <div><input type="time" min="14:00:00" max="12:00:00" value="15:00:00" id="test76"></div> <output id="test90a">a</output> <select id="test107"></select> </div> <div class="offscreen"> <div id="test89" style="width:1em">a &#x3000; b</div> <div id="ctrl89" style="width:1em">a b</div> <span id="test81a"></span><span id="test81b"></span> <div id="test95a" style="width: min-content; hyphens: auto; border: 1px solid red">2020-1</div> <div id="test95b" style="width: min-content; hyphens: auto; border: 1px solid red">2020-12020-1</div> <div><audio id="tzpPreload"></audio></div> </div> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#feature">return to TZP index</a></td></tr> </table> <table id="tb3"> <col width="1%"><col width="33%"><col width="33%"><col width="32%"><col width="1%"> <thead><tr><th colspan="5"> <div class="nav-title">gecko versions <div class="nav-up"><span class="c perf" id="perf"></span></div> </div> </th></tr></thead> <tr><td colspan="5" class="intro"> <span class="no_color"> <details class="no_color"><summary>INFO</summary> <ul> <li><span class='s3'>RESULT: </span><b><span class='good'> &#x2713; </span></b> &nbsp; pass &nbsp; <b><span class='bad'> &#x2715; </span></b> &nbsp; fail &nbsp; <b><span class='s4'> &#x2715; </span></b> &nbsp; error &nbsp; <b> &#x2715; </b> &nbsp; n/a </li> <li><span class='s3'>NOTES:</span> <ul> <li><span class='mono spaces'>PREF: </span>Fast-path with no false positives. Depends on a pref. Ideally should have a fallback</li> <li><span class='mono spaces'>DOM : </span>Touches the DOM, which we have to await. Can be slow. Is susceptible to being manipulated</li> <li><span class='mono spaces'>EVAL: </span>Requires <code>eval()</code> to avoid <code>SyntaxError</code> in earlier versions. Not universal, e.g. <code>CSP</code>. </li> <li><span class='mono spaces'>DATE INTL TMPR: Anything to do with dates or Intl APIs or Temporal</span> <ul> <li>are often targeted by <code>extensions</code></li> <li>may be affected by builds e.g. <code>--with-system-icu</code></li> </ul> </li> </ul> </li> <li><span class='s3'>VERSION:</span> <ul><li><b>Not <u>guaranteed</u> to be 100% correct</b>. Ideally, we don't want to <b><u>rely</u></b> on any possibly problematic tests (see NOTES above), but could use them as fast paths or fallbacks, as long as we don't get false positives. For the 0.01% of users that have or cause an "incorrect" version detection, that only increases their entropy in a full fingerprinting test </li></ul> </li> </ul> </details> </span> </td></tr> <!-- tzp --> <tr><td colspan="4"><hr></td><td></td></tr> <tr><td></td><td colspan="4"><span class="s3">TZP</span><span id="perftzp" class="c mono spaces"></span></td></tr> <tr><td></td> <td class="c mono spaces" id="tzp1" style="vertical-align: top;"></td> <td class="c mono spaces" id="tzp2" style="vertical-align: top;"></td> <td class="c mono spaces" id="tzp3" style="vertical-align: top;"></td> <td></td> </tr> <tr><td></td><td colspan="3" class="c mono spaces" id="alerttzp1"></td><td></td></tr> <tr><td></td><td colspan="3" class="c mono spaces" id="alerttzp2"></td><td></td></tr> <!-- other--> <tr><td colspan="4"><hr></td><td></td></tr> <tr><td></td><td colspan="4"><span class="s3">OTHER</span><span id="perfother" class="c mono spaces"></span></td></tr> <tr><td></td> <td class="c mono spaces" id="other1" style="vertical-align: top;"></td> <td class="c mono spaces" id="other2" style="vertical-align: top;"></td> <td class="c mono spaces" id="other3" style="vertical-align: top;"></td> <td></td> </tr> <tr><td></td><td colspan="3" class="c mono spaces" id="alertother1"></td><td></td></tr> <tr><td></td><td colspan="3" class="c mono spaces" id="alertother2"></td><td></td></tr> <!-- watch --> <!--<tr><td colspan="4"><hr></td><td></td></tr> <tr><td></td><td colspan="4"><span class="s3">WATCH</span></td></tr> <tr><td></td><td colspan="3" class="c mono spaces" id="watchlist"></td><td></td></tr>--> <!-- debug--> <tr><td colspan="4"><hr></td><td></td></tr> <tr><td></td><td colspan="4"><span class="s3">DEBUG</span></td></tr> <tr><td></td><td colspan="3" class="c mono spaces" id="debug"></td><td></td></tr> </table> <br> <script> 'use strict'; let debug = [] function outputVersion() { let xS = green_tick, xF = red_cross, xB = yellow_block, xNA = white_na, xNC = sb +"no change "+ sc, xOMG = sg +"has landed"+ sc, strA = "<a href='https://bugzilla.mozilla.org/", strB = "' class='blue' target='_blank'>", strC = "</a>", t0 = performance.now() debug.push("engine: "+ isEnginePretty.trim() +"<br>") debug.push(" gecko: "+ isFFpretty.trim() +"<br>") let gTZP, gOther, gWatch // global timers let runtype function output(type) { // vars let results = [], expected, elAlert // data -> results in descending order if (type == "tzp") { const names = Object.keys(oTZP).sort((a,b) => b-a) for (const k of names) {results.push(oTZP[k])} expected = expectTZP elAlert = dom.alerttzp2 } else if (type == "other") { const names = Object.keys(oOther).sort((a,b) => b-a) for (const k of names) {results.push(oOther[k])} expected = expectOther elAlert = dom.alertother2 } else { const names = Object.keys(oWatch).sort((a,b) => b-a) for (const k of names) {results.push(oWatch[k])} expected = expectWatch elAlert = dom.alertwatch2 } // we duped on unique object names if (results.length !== expected) { elAlert.innerHTML = sb.trim() + "DISPLAY ALERT: expected "+ expected +" got "+ results.length + sc + "<br>" } // build display data let display = [], prevV = "", prevB = "", prefix = 0 for (let i=0; i < results.length; i++) { let id = results[i].split("~")[0].trim(), bug = results[i].split("~")[1], result = results[i].split("~")[2], perf = results[i].split("~")[3], note = results[i].split("~")[4] // round perf perf = Math.round(perf*1) // version if (Number.isInteger(id * 1)) { if (id !== prevV) {id = s3 + id.padStart(3) + sc} else {id = "".padStart(3)} } // note note = ' '+ (note.substring(0,4)).padEnd(4) // bugzilla if (bug !== "") { bug = bug +"" prefix = 9 - bug.length bug = "".padStart(prefix) + strA + bug + strB + bug + strC + note } else { bug = "".padStart(9) + note } // perf perf = perf * 1 // color up legit slow perf = (perf > 1 && perf < 50 ? sb + (perf + "").padStart(4) + sc : (perf + "").padStart(4)) + " ms" display.push(id + bug + result + perf) prevV = results[i].split("~")[0].trim() prevB = results[i].split("~")[1] } // split into three columns: make sure we split with a version number let cutpoint = Math.ceil(display.length/3) let splitpoint = "" for (let i = cutpoint; i < cutpoint + 5; i++) { let test = display[i] if (splitpoint == "" & test.slice(0,1) !== " ") {splitpoint = i} } let display1 = display.slice(0, splitpoint) // split the rest in half display = display.slice(splitpoint, display.length) cutpoint = Math.ceil(display.length/2) splitpoint = "" for (let i = cutpoint; i < cutpoint + 5; i++) { let test = display[i] if (splitpoint == "" & test.slice(0,1) !== " ") {splitpoint = i} } let display2 = display.slice(0, splitpoint) let display3 = display.slice(splitpoint, display.length) // output document.getElementById(type+"1").innerHTML = display1.join("<br>") document.getElementById(type+"2").innerHTML = display2.join("<br>") document.getElementById(type+"3").innerHTML = display3.join("<br>") // debug if (debug.length) {dom.debug.innerHTML = debug.join("<br>")} } let countTZP = 0, expectTZP = 110, maxVer = 152, oTZP = {}, oResults = {} let is144 = 'undefined' === typeof CSS2Properties function run_tzp() { // start global timer gTZP = performance.now() // record function rec(order, version, bug, result, perf, note = '') { //dom.perf.innerHTML = 'main: '+ order oResults[order] = [version, (performance.now() - gTZP) +" ms", result] // do as little as possible to not affect perf if (result === true) { result = xS //oResults[order] = version +"~"+ } else if (result === false || result === undefined || result == "") { result = xF } oTZP[order] = version +"~"+ bug +"~"+ result +"~"+ perf +"~"+ note countTZP++ // now do stuff after we have finished if (countTZP == expectTZP) { let totalPerf = Math.round((performance.now() - gTZP)) let strSuccess = "" if (isFF) { //console.debug(oResults) let results = [] const names = Object.keys(oResults).sort((a,b) => b-a) for (const k of names) {results.push(oResults[k])} // loop: stop at first success: so this covers dual tests where one fails and one passes // e.g. [105, "0 ms", true] let myVer = "", myPerf = "" for (let i = 0; i < results.length; i++) { let array = results[i] if (array[2]) { myVer = array[0] myPerf = array[1] break } } // cleanup false results let is52 = false if (myVer == "") {is52 = true} if ("function" !== typeof CSSMozDocumentRule) {is52 = true} // Palemoon/Basilisk // waterfox classic: 57 pass, 56 fail if ("function" === typeof AbortSignal && "undefined" !== typeof HTMLAppletElement) { // but waterfox (v78) also fails 56, so test that as well if (!window.Document.prototype.hasOwnProperty("replaceChildren")) { is52 = true } } if (is52) { myVer = "52 or lower" myPerf = totalPerf +" ms" } if (myVer > (maxVer - 1)) {myVer += "+"} strSuccess = " "+ s14 + " [result: FF"+ myVer +" | "+ myPerf +"]"+ sc } dom.perftzp.innerHTML = "".padStart(3) + expectTZP +" tests | "+ totalPerf +" ms" + strSuccess output("tzp") } else if (countTZP > expectTZP) { dom.alerttzp1.innerHTML = sb.trim() + " RESULT ALERT: expected "+ expectTZP +" got "+ countTZP + sc } } function testperf() { let tStart = performance.now(), testversion // code here let tEnd = performance.now() console.log(tEnd - tStart, 'ms', testversion) } // 152: 2034371 let t152 = performance.now(), ver152 try { if (SVGTextPathElement.prototype.hasOwnProperty('side')) {ver152 = true} } catch(e) {} rec(152, 152, "2034371", ver152, performance.now()-t152) // 0.06 ms // 151: 2024291 let t151a = performance.now(), ver151a try { Temporal.PlainMonthDay.from({calendar:'chinese', year: 1898, monthCode:'M01L', day: 29}, {overflow: 'reject'}) } catch(e) { if ('RangeError: calendar field "day" is too large: 29' == e+'') ver151a = true } rec(151.1, 151, "2024291", ver151a, performance.now()-t151a, 'TMPR') // 0.06 ms if no error | 0.35 ms is 151+ // 151: 2022827 let t151 = performance.now(), ver151 try { if (CSSContainerRule.prototype.hasOwnProperty('conditions')) {ver151 = true} } catch(e) {} rec(151, 151, "2022827", ver151, performance.now()-t151) // 0.005 ms // 150: 1801658 let t150 = performance.now(), ver150 try { ver150 = 'object' == typeof visualViewport.onscrollend } catch(e) {} rec(150, 150, "1801658", ver150, performance.now()-t150) // 0.005 ms // 149: 2009792 let t149 = performance.now(), ver149 try { Temporal.PlainDate.from({calendar: 'gregory', monthCode: 'M12', month: 13, year: 2019, day: 1}) } catch(e) { if ('RangeError' == e.name) {ver149 = true} } rec(149, 149, "2009792", ver149, performance.now()-t149, 'TMPR') // 0.02 !temporal, 0.05 false, 0.07 true // 148: 1085214 // fast-path: pref dom.location.ancestorOrigins.enabled (added + default true FF148+) let t148a = performance.now(), ver148a try { ver148a = undefined !== location.ancestorOrigins } catch(e) {} rec(148.1, 148, "1085214", ver148a, performance.now()-t148a, 'PREF') // 0.01ms // 148: 2004851 let t148 = performance.now(), ver148 try { let test148 = new Temporal.Duration(0).total({unit:'years', relativeTo:'-271821-04-19'}) ver148 = true } catch(e) {} rec(148, 148, "2004851", ver148, performance.now()-t148, 'TMPR') // 0.07ms fail | 0.04ms success // 147: 2000225 (not confirmed) let t147 = performance.now(), ver147 try {ver147 = Intl.supportedValuesOf('numberingSystem').includes('tols')} catch(e) {} rec(147, 147, "2000225", ver147, performance.now()-t147, 'INTL') // 0.02ms // 146: 1997216 let t146 = performance.now(), ver146 try {throw new DOMException('a', 'b')} catch(e) {ver146 = e.columnNumber !== 0} rec(146, 146, "1997216", ver146, performance.now()-t146) // 0.02ms // 145: 1968987 let t145 = performance.now(), ver145 try { ver145 = undefined !== (new ToggleEvent('toggle', null)).source } catch(e) {} rec(145, 145, "1968987", ver145, performance.now()-t145) // 0.02ms // 144: 1919582 let t144 = performance.now() let ver144 = undefined == window.CSS2Properties rec(144, 144, "1919582", ver144, performance.now()-t144) // 0.005ms // 143: 1977489 // fast-path: layout.css.moz-appearance.webidl.enabled (default false) let t143 = performance.now(), ver143 try { ver143 = (is144 || 'function' === typeof CSS2Properties && !CSS2Properties.prototype.hasOwnProperty('-moz-appearance')) } catch(e) {} rec(143, 143, "1977489", ver143, performance.now()-t143, 'PREF') // // 142: 1960300 // note: intl.icu4x.segmenter.enabled has no effect on this test let t142 = performance.now(), ver142 try { let segmenter = new Intl.Segmenter('en', {granularity:'word'}) let test142 = Array.from(segmenter.segment('a:b')).map(({ segment }) => segment) if (3 == test142.length) {ver142 = true} } catch(e) {} rec(142, 142, "1960300", ver142, performance.now()-t142, 'INTL') // 0.15ms // 141: 1860030 // fast-path: pref: dom.intersection_observer.scroll_margin.enabled (added + default true FF141+) let t141a = performance.now(), ver141a try {ver141a = window["IntersectionObserver"].prototype.hasOwnProperty('scrollMargin')} catch(e) {} rec(141.1, 141, "1860030", ver141a, performance.now()-t141a, "PREF") // 0.005ms // 141: 1950162: // fast-path: requires temporal (default enabled FF139+) javascript.options.experimental.temporal let t141 = performance.now(), ver141 try {ver141 = undefined == Temporal.PlainDate.from('2029-12-31[u-ca=gregory]').weekOfYear} catch(e) {} rec(141, 141, "1950162", ver141, performance.now()-t141, "PREF") // 0.06ms (0.02 if error) // 140: 1550462 // fast-path: pref: dom.event.pointer.rawupdate.enabled (added + default true FF140+) let t140b = performance.now(), ver140b try {ver140b = "object" === typeof onpointerrawupdate} catch(e) {} rec(140.2, 140, "1550462", ver140b, performance.now()-t140b, "PREF") // 0.003ms // 140: 1963464 // can't find any prefs that disable paint: added FF84+: https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming let t140a = performance.now(), ver140a try { let entries140 = performance.getEntriesByType("paint") for (let i=0; i < entries140.length; i++) { if ('[object PerformancePaintTiming]' === entries140[i] +'') { ver140a = undefined !== entries140[i].presentationTime } } } catch(e) {} rec(140.1, 140, "1963464", ver140a, performance.now()-t140a) // 0.04ms // 140: 929890: 139 = "", 140+ = "auto" : also see 1969210 let t140 = performance.now(), ver140 try {ver140 = '' !== dom.tzpPreload.preload} catch(e) {} rec(140, 140, "929890", ver140, performance.now()-t140, "DOM") // 0.04ms // 139: 1960556 let t139 = performance.now() , ver139 try {ver139 = HTMLDialogElement.prototype.hasOwnProperty('requestClose')} catch(e) {} rec(139, 139, "1960556", ver139, performance.now()-t139) // 0.005ms // 138: 1525241 // fast-path: requires webrtc // e.g. media.peerconnection.enabled | --disable-webrtc let t138b = performance.now(), ver138b try { ver138b = RTCCertificate.prototype.hasOwnProperty('getFingerprints') } catch(e) {} rec(138.2, 138, "1525241", ver138b, performance.now()-t138b, "PREF") // 0.004ms // 138: fast-path: dom.origin_agent_cluster.enabled let t138a = performance.now() let ver138a = 'boolean' == typeof originAgentCluster rec(138.1, 138, "1665474", ver138a, performance.now()-t138a, "PREF") // 0.002ms // 138: 1954425: must be FF134 or higher let t138 = performance.now(), ver138, test138 try { if (HTMLScriptElement.prototype.hasOwnProperty('textContent')) { // FF135+ test138 = Intl.NumberFormat('yo-bj', {style: 'unit', unit: 'year', unitDisplay: 'narrow'}).format(1) ver138 = '606d1046' == mini(test138) } } catch(e) {} rec(138, 138, "1954425", ver138, performance.now()-t138, "INTL") // 6ms cold, 0.3ms with TZP's NumberFormat warmup // 137: 1943120 // javascript.options.experimental.math_sumprecise // fast-path 136 ifdef NIGHTLY_BUILD false | 137 true all releases let t137 = performance.now(), ver137 try { ver137 = 'function' == typeof Math.sumPrecise } catch(e) {} rec(137, 137, "1943120", ver137, performance.now()-t137, "PREF") // 0.002ms // 136: 1939533 // fast-path: FF132+ pref enabled javascript.options.experimental.regexp_modifiers // note: TB slider's `javascript.options.native_regexp` (requires restart) has no effect let t136 = performance.now(), ver136 try { ver136 = (new RegExp("(?i:[A-Z]{4})")).test('abcd') } catch(e) {} // FF131 or lower: SyntaxError: invalid regexp group rec(136, 136, "1939533", ver136, performance.now()-t136, "PREF") // 0.02ms // 135: 1905706 let t135 = performance.now() let ver135 = HTMLScriptElement.prototype.hasOwnProperty('textContent') rec(135, 135, "1905706", ver135, performance.now()-t135) // 0.004ms // 134: 1927706 : ie, lij, oc, st, szl, tn, za // packages that use --with-system-icu can cause issues let t134 = performance.now(), ver134 try {ver134 = ("lij" == Intl.PluralRules.supportedLocalesOf("lij").join())} catch(e) {} rec(134, 134, "1927706", ver134, performance.now()-t134, "INTL") // 0.125ms // 133: 1837773 let t133 = performance.now(), ver133 try { let parser = (new DOMParser).parseFromString("<select><option name=''></option></select>", 'text/html') ver133 = null === parser.body.firstChild.namedItem('') } catch(e) {} rec(133, 133, "1837773", ver133, performance.now()-t133) // 0.2ms // 132: 1899413 let t132 = performance.now(), ver132 = false try { const re = new RegExp('(?:)', 'gv'); let test132 = RegExp.prototype[Symbol.matchAll].call(re, '𠮷') for (let i=0; i < 3; i++) {ver132 = test132.next().done} } catch(e) {} rec(132, 132, "1899413", ver132, performance.now()-t132) // 0.08ms | 0.03 if error // 131: 1900196 // false posiitve in FF78 or lower let t131 = performance.now(), ver131 = false if (window.hasOwnProperty("UserActivation")) { // FF120+ try { let test131 = new Intl.DateTimeFormat('zh', { calendar: 'chinese', dateStyle: 'medium'}).format(new Date(2033, 9, 1)) ver131 = '2033' == test131.slice(0,4) } catch(e) {} } rec(131, 131, "1900196", ver131, performance.now()-t131, 'INTL') // slow AF // 130: 1907236 // false posiitve in FF77 or lower let t130 = performance.now(), ver130 = false if (window.hasOwnProperty("UserActivation")) { // FF120+ try { new RegExp('[\\00]','u') } catch(e) { if (e+'' == 'SyntaxError: invalid decimal escape in regular expression') {ver130 = true} } } rec(130, 130, "1907236", ver130, performance.now()-t130) // 0.025ms // 129: 1595620 let t129 = performance.now() let ver129 = (is144 || "function" === typeof CSS2Properties && CSS2Properties.prototype.hasOwnProperty("WebkitFontFeatureSettings")) rec(129, 129, "1595620", ver129, performance.now()-t129) // 0.6ms // 128: 1896509 let t128 = performance.now(), ver128 = false try { let test128 = (new Blob()).bytes() ver128 = true } catch(e) {} rec(128, 128, "1896509", ver128, performance.now()-t128) // 0.03ms // 127: 1894248 let t127 = performance.now(), ver127 = false try {ver127 = (new Date('15Jan0024')).getYear() > 0} catch(e) {} rec(127, 127, "1894248", ver127, performance.now()-t127, "DATE") // 0.07ms // 126: 1887611 let t126 = performance.now(), ver126 = false try {ver126 = "function" === typeof URL.parse} catch(e) {} rec(126, 126, "1887611", ver126, performance.now()-t126) // 0.25ms // 125: 1872793 let t125 = performance.now(), ver125 = false try { ver125 = "Invalid Date" == new Date("Sep 26 Thurs 1995 10:00") } catch(e) {} rec(125, 125, "1872793", ver125, performance.now()-t125, "DATE") // 0.05ms when invalid, 0.2+ when valid // 124: 1867569 (slow, like 122) let t124 = performance.now(), ver124 = false try { let el124 = document.documentElement el124.style.zIndex = "calc(1 / max(-0, 0))" // default = auto let test124 = getComputedStyle(el124).zIndex debug.push("124 1867569 : "+ test124) if (test124 > 0) {ver124 = true} el124.style.zIndex = "auto" } catch(e) {} rec(124, 124, "1867569", ver124, performance.now()-t124) // // 123: 1871745 let t123 = performance.now() let ver123 = (is144 || "function" === typeof CSS2Properties && !CSS2Properties.prototype.hasOwnProperty("MozUserFocus")) rec(123, 123, "1871745", ver123, performance.now()-t123) // 122: 1867558 (slow) let t122 = performance.now(), ver122 = false try { let el122 = document.documentElement el122.style.zIndex = "calc(1 / abs(-0))" // default = auto let test122 = getComputedStyle(el122).zIndex debug.push("122 1867558 : "+ test122) if (test122 > 0) {ver122 = true} el122.style.zIndex = "auto" } catch(e) {} rec(122, 122, "1867558", ver122, performance.now()-t122) // 0.73ms // 121: 1845586 let t121 = performance.now() let ver121 = ("function" === typeof Promise.withResolvers) rec(121, 121, "1845586", ver121, performance.now()-t121) // // 120: 1791079 let t120 = performance.now() let ver120 = (window.hasOwnProperty("UserActivation")) rec(120, 120, "1791079", ver120, performance.now()-t120) // 0.017ms // 119: 1817591 let t119 = performance.now() try { location.href = "http://a>b/" } catch(e) { rec(119, 119, "1817591", e.name === "SyntaxError", performance.now()-t119) } // 118: 1849010 let t118 = performance.now() let ver118 = (is144 || "function" === typeof CSS2Properties && CSS2Properties.prototype.hasOwnProperty("fontSynthesisPosition")) rec(118, 118, "1849010", ver118, performance.now()-t118) // 117: 1842467 let t117 = performance.now() let ver117 = (CanvasRenderingContext2D.prototype.hasOwnProperty("fontStretch")) rec(117, 117, "1842467", ver117, performance.now()-t117) // 116: 1839614 let t116 = performance.now() let ver116 = (CanvasRenderingContext2D.prototype.hasOwnProperty("textRendering")) rec(116, 116, "1839614", ver116, performance.now()-t116) // 115: 1778909 let t115 = performance.now() let ver115 = (CanvasRenderingContext2D.prototype.hasOwnProperty("letterSpacing")) rec(115, 115, "1778909", ver115, performance.now()-t115) // 114: 1826629 // slow ~2ms let t114 = performance.now() let ver114 = (is144 || "function" === typeof CSS2Properties && CSS2Properties.prototype.hasOwnProperty("WebkitTextSecurity")) rec(114, 114, "1826629", ver114, performance.now()-t114) // 113: 1709347 let t113 = performance.now() let ver113 = (CanvasRenderingContext2D.prototype.hasOwnProperty("reset")) rec(113, 113, "1709347", ver113, performance.now()-t113) // 112: 1756175 let t112 = performance.now() let ver112 = (CanvasRenderingContext2D.prototype.hasOwnProperty("roundRect")) // 0.01ms rec(112, 112, "1756175", ver112, performance.now()-t112) // 111: 1418449 let t111 = performance.now() let ver111 = (HTMLElement.prototype.hasOwnProperty("translate")) // 0.1ms rec(111, 111, "1418449", ver111, performance.now()-t111) // // 110: 1689631 let t110 = performance.now() let ver110 = ("object" === typeof ondeviceorientationabsolute) rec(110, 110, "1689631", ver110, performance.now()-t110) // 0.01ms // 109: 1789776 let t109 = performance.now() let ver109 = (CSSKeyframesRule.prototype.hasOwnProperty("length")) // 0.01ms rec(109, 109, "1789776", ver109, performance.now()-t109) // 108: 1574487 let t108 = performance.now() let ver108 = ("undefined" === typeof onloadend) rec(108, 108, "1574487", ver108, performance.now()-t108) // 107: 1174097 let t107 = performance.now() let ver107 = (!SVGSVGElement.prototype.hasOwnProperty("useCurrentView")) // 0.07ms rec(107, 107, "1174097", ver107, performance.now()-t107) // 106: 1777293 let t106 = performance.now() let ver106 = (Element.prototype.hasOwnProperty("checkVisibility")) // 0.009 ms rec(106, 106, "1777293", ver106, performance.now()-t106) // 105: 830716 // 105: Function object could not be cloned. 36 // 94-104: The object could not be cloned. 31 // 93-: structuredClone is not defined. 30 let t105 = performance.now(), ver105 try {structuredClone((() => {}))} catch(e) {ver105 = (e.message.length == 36)} rec(105, 105, "830716", ver105, performance.now()-t105) // 0.15ms // 104: 1712623 let t104 = performance.now(), ver104 if (undefined !== window.SVGStyleElement) { // check window prop first e.g. servo ver104 = (SVGStyleElement.prototype.hasOwnProperty("disabled")) } rec(104, 104, "1712623", ver104, performance.now()-t104) // 103: 1772494 let t103 = performance.now() let ver103 = (undefined === new ErrorEvent("error").error) rec(103, 103, "1772494", ver103, performance.now()-t103) // 102: 1767541: regression FF99, hence check for v101 let t102 = performance.now() let ver102 = (CanvasRenderingContext2D.prototype.hasOwnProperty("direction") && Array(1).includes()) rec(102, 102, "1767541", ver102, performance.now()-t102) // 101: 1728999 let t101 = performance.now(), ver101 = (CanvasRenderingContext2D.prototype.hasOwnProperty("direction")) rec(101, 101, "1728999", ver101, performance.now()-t101) // 100: 1753309: legacy check: FF56- AbortSignal let t100 = performance.now(), ver100 = ("function" === typeof AbortSignal && "function" === typeof AbortSignal.timeout) rec(100, 100, "1753309", ver100, performance.now()-t100) // 99: 1711715 + 1756204 // note: FF90+ javascript.options.experimental.private_fields : default true let t99 = performance.now(), ver99 try { newFn("class A { #x; h(o) { return !#x in o; }}") } catch(e) { // 99+ : invalid use of private name in unary expression without object reference (72) (pref has no effect) // 98- : private names aren't valid in this context (pref = true) // 98- : private fields are not currently supported (pref = false / no-pref) //debug.push(" 99 1711715 : "+ e.name +" : "+ e.message) ver99 = (e.message.length == 72) } rec(99, 99, "1711715", ver99, performance.now()-t99, "EVAL") // 98: 1709790 let t98 = performance.now(), ver98 = (HTMLElement.prototype.hasOwnProperty("outerText")) rec(98, 98, "1709790", ver98, performance.now()-t98) // 97: 1745372: legacy check: FF56- AbortSignal let t97 = performance.now(), ver97 = ("function" === typeof AbortSignal && "function" === typeof AbortSignal.prototype.throwIfAborted) rec(97, 97, "1745372", ver97, performance.now()-t97) // 96: 1738422: legacy check + perf: toSource (74+): FF68- very slow: FF57- Intl.PluralRules let t96 = performance.now(), ver96 try {ver96 = ("undefined" === typeof Object.toSource && "sc" == Intl.PluralRules.supportedLocalesOf("sc").join())} catch(e) {ver96 = xB} rec(96, 96, "1738422", ver96, performance.now()-t96, "INTL") // 95: 1723674: fast path: dom.crypto.randomUUID.enabled = true (default) let t95p = performance.now(), ver95p = ("function" === typeof crypto.randomUUID) rec(95.1, 95, "1723674", ver95p, performance.now()-t95p, "PREF") // 95: 1674204 : slow // note: en-US, windows // FF60-65 wraps identical : 93/93 = 1 (offscreen: if not offscreen uses page width) // FF66-94 wraps 2020-1 vs 2020-12 : 45/53 = 0.8490566037735849 // FF95+ wraps 2020-1 x twice that : 47/93 = approx 0.5 let t95 = performance.now(), ver95 try { let w95 = dom.test95a.offsetWidth, h95 = dom.test95b.offsetWidth let test95 = w95/h95 debug.push(" 95 1674204 : "+ w95 +"/"+ h95 +" = " +test95) ver95 = (test95 > 0.4 && test95 < 0.6) } catch(e) { ver95 = xB } rec(95, 95, "1674204", ver95, performance.now()-t95, "DOM") // 94: 1722576 let t94 = performance.now(), ver94 = ("function" === typeof self.structuredClone) rec(94, 94, "1722576", ver94, performance.now()-t94) // 93: 1722448 let t93 = performance.now(), ver93 = ("function" === typeof self.reportError) rec(93, 93, "1722448", ver93, performance.now()-t93) // 92: 1721149 let t92 = performance.now(), ver92 = ("function" === typeof Object.hasOwn) rec(92, 92, "1721149", ver92, performance.now()-t92) // 91: 1717072: fast path: dom.window.clientinformation.enabled = true (default) let t91p = performance.now(), ver91p = ("object" === typeof window.clientInformation) rec(91.1, 91, "1717072", ver91p, performance.now()-t91p, "PREF") // 91: 1714933: legacy perf: toSource (74+): FF70- very slow let t91 = performance.now(), ver91 try {ver91 = ("undefined" === typeof Object.toSource && "sa" == Intl.Collator.supportedLocalesOf("sa").join())} catch(e) {ver91 = xB} rec(91, 91, "1714933", ver91, performance.now()-t91, "INTL") // 90: 1681371 shipped let t90 = performance.now(), ver90 = ("function" == typeof Array.prototype.at) rec(90, 90, "1681371", ver90, performance.now()-t90) // 89: 1684316: legacy check: FF64- CountQueuingStrategy let t89 = performance.now(), ver89 = ("function" === typeof CountQueuingStrategy && ! new CountQueuingStrategy({highWaterMark: 1}).hasOwnProperty("highWaterMark")) rec(89, 89, "1684316", ver89, performance.now()-t89) // 88: 1497557 let t88 = performance.now(), ver88 = (":" === document.createElement("a").protocol) rec(88,88, "1497557", ver88, performance.now()-t88) // 87: 1688335 let t87 = performance.now(), ver87 = (undefined === console.length) rec(87, 87, "1688335", ver87, performance.now()-t87) // 86: 1654116 shipped let t86 = performance.now(), ver86 = ("function" === typeof Intl.DisplayNames) rec(86, 86, "1654116", ver86, performance.now()-t86) // 85: 1675240 // 85+: RegExp.prototype.global getter called on non-RegExp object: string // 84-: get global method called on incompatible string let t85 = performance.now(), ver85 try { Object.getOwnPropertyDescriptor(RegExp.prototype, "global").get.call("/a") } catch(e) { ver85 = (e.message.length == 66) } rec(85, 85, "1675240", ver85, performance.now()-t85) // 84: 1518999 let t84 = performance.now(), ver84 = ("function" === typeof PerformancePaintTiming) rec(84, 84, "1518999", ver84, performance.now()-t84) // 83: 1665252: legacy check: toSource (74+): FF55- false positive let t83 = performance.now(), ver83 = ("undefined" === typeof Object.toSource && !window.HTMLIFrameElement.prototype.hasOwnProperty("allowPaymentRequest")) rec(83, 83, "1665252", ver83, performance.now()-t83) // 82: 1655947 // ext fuckery: cydec let t82 = performance.now(), ver82 try {ver82 = (1595289600000 === Date.parse("21 Jul 20 00:00:00 GMT"))} catch(e) {ver82 = xB} rec(82, 82, "1655947", ver82, performance.now()-t82, "DATE") // 81: 1650607: legacy check: toSource (74+): 52 false positive let t81 = performance.now(), ver81 = ("undefined" === typeof Object.toSource && new File(["x"], "a/b").name == "a/b") rec(81, 81, "1650607", ver81, performance.now()-t81) // 80: 1620467 // blink check: CSS2Properties let t80 = performance.now() let ver80 = (is144 || "function" === typeof CSS2Properties && CSS2Properties.prototype.hasOwnProperty("appearance")) rec(80, 80, "1620467", ver80, performance.now()-t80) // 79: 1599769 shipped let t79 = performance.now(), ver79 = ("function" === typeof Promise.any) rec(79, 79, "1599769", ver79, performance.now()-t79) // 78: 1626015 let t78 = performance.now(), ver78 = (window.Document.prototype.hasOwnProperty("replaceChildren")) rec(78, 78, "1626015", ver78, performance.now()-t78) // 77: 1536540 let t77 = performance.now(), ver77 if (undefined !== window.IDBCursor) { // checked for window proeprty for e.g. servo ver77 = (window.IDBCursor.prototype.hasOwnProperty("request")) } rec(77, 77, "1536540", ver77, performance.now()-t77) // 76: 1608010: legacy check toSource (74+) FF56- false positive let t76 = performance.now(), ver76 = ("undefined" === typeof Object.toSource && !test76.validity.rangeOverflow) rec(76, 76, "1608010", ver76, performance.now()-t76, "DOM") // 75: 1613713 let t75 = performance.now(), ver75 = ("function" === typeof Intl.Locale) rec(75, 75, "1613713", ver75, performance.now()-t75) // 74 1565170 let t74 = performance.now(), ver74 = ("undefined" === typeof Object.toSource) rec(74, 74, "1565170", ver74, performance.now()-t74) // 73: 1602163 let t73 = performance.now(), ver73 = ("function" === typeof VideoPlaybackQuality && !VideoPlaybackQuality.prototype.hasOwnProperty("corruptedVideoFrames")) rec(73, 73, "1602163", ver73, performance.now()-t73) // 72: 1591892 let t72 = performance.now(), ver72 = ("boolean" === typeof self.crossOriginIsolated) rec(72, 72, "1591892", ver72, performance.now()-t72) // 71: 1549176 let t71 = performance.now(), ver71 = ("function" === typeof Promise.allSettled) rec(71, 71, "1549176", ver71, performance.now()-t71) // 70: 1473229: legacy check: FF64- Intl.RelativeTimeFormat // ext fuckery: chameleon let t70e = performance.now(), ver70e = ("function" === typeof Intl.RelativeTimeFormat && "function" === typeof Intl.RelativeTimeFormat.prototype.formatToParts) rec(70.1, 70, "1473229", ver70e, performance.now()-t70e, "INTL") // 70: 1435818: fallback let t70 = performance.now(), ver70 try {newFn("let t = 1_050"); ver70 = true} catch(e) {} rec(70, 70, "1435818", ver70, performance.now()-t70, "EVAL") // 69: 1557121 let t69 = performance.now(), ver69 = ("function" === typeof Blob.prototype.text) rec(69, 69, "1557121", ver69, performance.now()-t69) // 68: 1548773 let t68 = performance.now(), ver68 = (!HTMLObjectElement.prototype.hasOwnProperty("typeMustMatch")) rec(68, 68, "1548773", ver68, performance.now()-t68) // 67: 1531830 let t67 = performance.now(), ver67 = ("function" === typeof String.prototype.matchAll) rec(67, 67, "1531830", ver67, performance.now()-t67) // 66: 1425685: legacy check: FF60- HTMLSlotElement let t66 = performance.now(), ver66 = ("function" === typeof HTMLSlotElement && "function" === typeof HTMLSlotElement.prototype.assignedElements) rec(66, 66, "1425685", ver66, performance.now()-t66) // 65: 1334813 let t65 = performance.now(), ver65 = (1 === DataView.length) rec(65, 65, "1334813", ver65, performance.now()-t65) // 64: 1498860 let t64 = performance.now(), ver64 = ("number" === typeof window.screenTop) rec(64, 64, "1498860", ver64, performance.now()-t64) // 63: 1472170 let t63 = performance.now(), ver63 = ("desc" === Symbol('desc').description) rec(63, 63, "1472170", ver63, performance.now()-t63) // 62: 1458466 let t62 = performance.now(), ver62 = ("function" === typeof console.timeLog) rec(62, 62, "1458466", ver62, performance.now()-t62) // 61: 1455805 let t61 = performance.now(), ver61 = ("object" === typeof CSS) rec(61, 61, "1455805", ver61, performance.now()-t61) // 60: 1436659 let t60 = performance.now(), ver60 if (undefined !== window.Animation) { // check for window property e.g. servo ver60 = ("function" === typeof Animation.prototype.updatePlaybackRate) } rec(60, 60, "1436659", ver60, performance.now()-t60) // 59: 1336400 let t59 = performance.now(), ver59 = (!HTMLMediaElement.prototype.hasOwnProperty("mozAutoplayEnabled")) rec(59, 59, "1336400", ver59, performance.now()-t59) // 58: 1403318 let t58 = performance.now(), ver58 = ("function" === typeof Intl.PluralRules) rec(58, 58, "1403318", ver58, performance.now()-t58) // 57: 1378342 let t57 = performance.now(), ver57 = ("function" === typeof AbortSignal) rec(57, 57, "1378342", ver57, performance.now()-t57) // 56: 1279218 let t56 = performance.now(), ver56 = ("function" !== typeof HTMLAppletElement) rec(56, 56, "1279218", ver56, performance.now()-t56) // 55: 1351795 let t55 = performance.now(), ver55 = ("undefined" === typeof console.timeline) rec(55, 55, "1351795", ver55, performance.now()-t55) // 54: 1337702 let t54 = performance.now(), ver54 = (URL.prototype.hasOwnProperty("toJSON")) rec(54, 54, "1337702", ver54, performance.now()-t54) // 53: let t53 = performance.now(), ver53 = ("function" === typeof CSSMozDocumentRule) rec(53, 53, "", ver53, performance.now()-t53) } run_tzp() let countOther = 0, expectOther = 70, oOther = {} function run_other() { // start global timer gOther = performance.now() // record function rec(order, version, bug, result, perf, note = "") { if (result === true) {result = xS } else if (result === false || result === undefined) {result = xF} oOther[order] = version +"~"+ bug +"~"+ result +"~"+ perf +"~"+ note countOther++ if (countOther == expectOther) { dom.perfother = "".padStart(3) + expectOther +" tests | "+ Math.round((performance.now() - gOther)) +" ms" output("other") } else if (countOther > expectOther) { dom.alertother1.innerHTML = sb.trim() + " RESULT ALERT: expected "+ expectOther +" got "+ countOther + sc } } // 150: 1561441: fast-path: requires webRTC let t150a = performance.now(), ver150a try { ver150a = 'function' == typeof window.RTCPeerConnectionIceErrorEvent } catch(e) {} rec(150.1, 150, "1561441", ver150a, performance.now()-t150a, 'RTC') // 0.004ms // 150: 2018544 // false positives if FF134-146 so check for 149 first let t150 = performance.now(), ver150 try { Temporal.PlainDate.from({calendar:'gregory', monthCode:'M12', month:13, year:2019, day:1}) } catch(e) { if ('RangeError' == e.name) { // FF149+ ver150 = 'Anno Domini 1970-01-01' == new Intl.DateTimeFormat('en-u-ca-iso8601', {era: 'long'}).format(0) } } rec(150, 150, "2018544", ver150, performance.now()-t150, 'TMPR') // ~1+ ms // 149: 1999917 = SLOW let t149 = performance.now(), ver149 try { ver149 = '/' !== new Intl.DateTimeFormat("en-u-ca-chinese", {timeZone:"UTC"}).format(new Date("+100000-01-01T00:00:00.000Z")) } catch(e) {} rec(149, 149, "1999917", ver149, performance.now()-t149, 'DATE') // 6.5ms // 147: 1666613: can't be used with base browser for when they backport it to ESR140 let t147 = performance.now(), ver147 try { let test147 = ((new DOMParser()).parseFromString('INVALID', 'text/xml')).firstChild.attributes[0].nodeValue ver147 = true } catch(e) {} rec(147, 147, "1666613", ver147, performance.now()-t147) // 0.17ms // 140: 1965802 SLOW let t140 = performance.now(), ver140 try { document.body.style.zIndex = 'calc(1 + 0.01)' ver140 = 'calc(1.01)' == document.body.style.zIndex document.body.style.zIndex = '' } catch(e) {} rec(140, 140, "1965802", ver140, performance.now()-t140) // 0.6ms // 139: 1960049 let t139 = performance.now(), ver139 try { ver139 = Intl.supportedValuesOf("timeZone").includes("America/Coyhaique") } catch(e) {} rec(139, 139, "1960049", ver139, performance.now()-t139, "INTL") // 0.3ms with warmup | 2ms cold // 136: 1945535: backported to 136 and ESR128 let t136 = performance.now(), ver136 try { // backported to ESR128, so check for 129+ first if (is144 || "function" === typeof CSS2Properties && CSS2Properties.prototype.hasOwnProperty("WebkitFontFeatureSettings")) { let test136 = new Date('July 2, 2025 0:00:00 UTC').toLocaleString('en', {timeZone: 'America/Asuncion'}) ver136 = '7/1/2025, 9:00:00 PM' == test136 // was -4, now -3 } } catch(e) {} rec(136, 136, "1945535", ver136, performance.now()-t136, 'DATE') // 0.25ms // 135: 1935198: pref `layout.css.moz-user-input.enabled` added as false // fast-path: if property missing you can't be any lower than 135: "MozUserInput","-moz-user-input" let t135c = performance.now() let ver135c = (is144 || "function" === typeof CSS2Properties && !CSS2Properties.prototype.hasOwnProperty("MozUserInput")) rec(135.3, 135, "1935198", ver135c, performance.now()-t135c, 'PREF') // 0.002ms // 135: 1930464 let t135b = performance.now(), ver135b try { // returns 2 in FF52 but lets avoid any possible old-timey regressions/changes // tested FF115-135 if (CanvasRenderingContext2D.prototype.hasOwnProperty('letterSpacing')) { // FF115+ let test135b = new Intl.NumberFormat('en-US', {style:'currency', currency:'USD', notation:'scientific'}) ver135b = 0 == test135b.resolvedOptions().minimumFractionDigits } } catch(e) {} rec(135.2, 135, "1930464", ver135b, performance.now()-t135b, 'INTL') // 0.2ms // 135: 1775215 let t135a = performance.now(), ver135a try { newFn('var else') } catch(e) { // SyntaxError: missing variable name, got keyword 'else' ver135a = (e+'').includes('else') } rec(135.1, 135, "1775215", ver135a, performance.now()-t135a, "EVAL") // 0.05ms // 135: 1930466 // this broke in FF142+, and even though we could wrap it in another test // and it doesn't affect any logic since we cascade down, let's just remove it /* let t135 = performance.now(), ver135 = false try { let test135 = new Intl.DateTimeFormat('en', {timeZone: 'Factory'}).format(new Date()) } catch(e) { if ('RangeError: invalid time zone in DateTimeFormat(): Factory' == e+'') ver135 = true } rec(135, 135, "1930466", ver135, performance.now()-t135) // very slow if not an error, slow if an eror //*/ // 134: 1927706 (see 1931044) // packages that use --with-system-icu can cause issues let t134 = performance.now(), ver134 try { // avoid old timey false positives e.g FF78-109 if (CanvasRenderingContext2D.prototype.hasOwnProperty('letterSpacing')) { // FF115+ ver134 = '$1.00' == (1).toLocaleString('en-CA', {style: 'currency', currencyDisplay: 'narrowSymbol', currency: 'USD'}) } } catch(e) {} rec(134, 134, "1927706", ver134, performance.now()-t134, "INTL") // 0.2ms // 133: 1922503 let t133 = performance.now(), ver133 try { let date133 = new Date('jan 15 2000') ver133 = date133.toLocaleString("en", {timeZone: "Asia/Choibalsan"}) == date133.toLocaleString("en", {timeZone: "Asia/Ulaanbaatar"}) } catch(e) {} rec(133, 133, "1922503", ver133, performance.now()-t133) // 0.3ms // 130: 1907369 let t130 = performance.now(), ver130 = false try { eval('class X { constructor() {} constructor() {} }') // eww, eval } catch(e) { if (e+'' == 'SyntaxError: a class cannot have more than one constructor definition') {ver130 = true} } rec(130, 130, "1907369", ver130, performance.now()-t130, "EVAL") // 0.09ms // 129: 1796785 let t129 = performance.now(), ver129 = false try { ver129 = ("function" === typeof PerformanceResourceTiming && PerformanceResourceTiming.prototype.hasOwnProperty("responseStatus")) } catch(e) {} rec(129, 129, "1796785", ver129, performance.now()-t129) // 0.04ms // 128: 1887817 // FF123-27: Error: Permission denied to access property "lastModified" // ^ if console closed: NS_ERROR_UNEXPECTED: // ^ we need to check a property otherwise no error is thrown if the console is open // FF52-122: TypeError: Document.parseHTMLUnsafe is not a function // relies on dom.webcomponents.shadowdom.declarative.enabled = true (flipped true in FF123) let t128 = performance.now(), ver128 = false try { let test128 = Document.parseHTMLUnsafe('<p></p>').lastModified ver128 = true } catch(e) {} rec(128, 128, "1887817", ver128, performance.now()-t128) // 0.08ms // 122: 1862910 let t122 = performance.now() let ver122 = false if ("function" === typeof Promise.withResolvers) { // 121 or lower is slow, so only check if 121+ ver122 = !isNaN(Date.parse("Mercredi 8 Septembre 2021")) ? true : false } rec(122, 122, "1862910", ver122, performance.now()-t122, "DATE") // 122: 0.1 | 121 = 1.1 // 120b: 1557650 let t120b = performance.now() let ver120b = (new Date("19999-11-11").toString() !== "Invalid Date") rec(120.2, 120, "1557650", ver120b, performance.now()-t120b, "DATE") // 0.22ms (on new rig) // 120a: 449921 let t120a = performance.now() let ver120a = (new Date("1JAN2008").toString() !== "Invalid Date") rec(120.1, 120, "449921", ver120a, performance.now()-t120a, "DATE") // 0.22ms (on new rig) // 120: 1852422 let t120 = performance.now() let ver120 = (new Date("01/01/2001 10:00Z").toString() !== "Invalid Date") rec(120, 120, "1852422", ver120, performance.now()-t120, "DATE") // 0.22ms (on new rig) // 116: 1769088 let t116 = performance.now() let ver116 = (isNaN(Date.parse("-000000-01-01T00:00:00.000Z"))) rec(116, 116, "1769088", ver116, performance.now()-t116, "DATE") // 0.015ms // 107: 344060 let t107 = performance.now(), ver107 // false positives FF52-57 if (Element.prototype.hasOwnProperty("checkVisibility")) { try { dom.test107.options.length = -1 ver107 = true } catch(e) {} // NotSupportedError Operation is not supported } rec(107, 107, "344060", ver107, performance.now()-t107, "DOM") // 101a: 1764050 let t101a = performance.now(), ver101a try { ver101a = Intl.supportedValuesOf("currency").includes("SLE") } catch(e) {} rec(101.1, 101, "1764050", ver101a, performance.now()-t101a, "INTL") // 101: 1752808 let t101 = performance.now(), ver101 try { // avoid old timey false positives under FF93 if ("function" === typeof self.structuredClone) { // FF94+ let test101 = new Intl.NumberFormat("en", {style:"currency", currency:"USD", maximumFractionDigits:2, notation:"compact"}).format(1773500) ver101 = (test101 == "$1.77M") debug.push("101 1752808 : "+ test101) } } catch(e) {} rec(101, 101, "1752808", ver101, performance.now()-t101, 'INTL') // 99: 1720353 // ext fuckery possible/likely due to mimeTypes/plugins let t99 = performance.now(), ver99 = ("pdfViewerEnabled" in navigator) rec(99, 99, "1720353", ver99, performance.now()-t99, "PREF") // 96: 1738422 // ext fuckery: chameleon, cydec let t96 = performance.now(), ver96 try { //let date96 = let test96 = new Date(Date.UTC(2012,12-1,6,12,0,0)).toLocaleString("zh-Hans-CN", {timeZone: "Asia/Shanghai", timeZoneName: "long"}) ver96 = (test96 == "2012/12/6 中国标准时间 20:00:00") } catch(e) { debug.push(" 96 1738422 : "+ e.name +": "+ e.message) ver96 = xB } rec(96,96, "1738422", ver96, performance.now()-t96, "DATE") // 94: 1729239 let t94 = performance.now(), ver94 = ("function" === typeof HTMLScriptElement.supports) rec(94, 94, "1729239", ver94, performance.now()-t94) // 93b: 1670033 let t93b = performance.now(), ver93b = ("function" === typeof Intl.supportedValuesOf) rec(93.1, 93, "1670033", ver93b, performance.now()-t93b) // 93a: 1328672 let t93a = performance.now(), ver93a try { ver93a = (!isNaN(new Date("1997-03-08 11:19:10-07").getTime())) } catch(e) { ver93a = xB } rec(93, 93, "1328672", ver93a, performance.now()-t93a) // 91b: 1284868 // ext fuckery: cydec let t91b = performance.now(), ver91b try { let test91b = new Intl.DateTimeFormat('en-US', {second: "2-digit", hour12: false}) ver91b = ("07" == test91b.format(new Date('2016/06/06 15:05:07'))) } catch(e) { ver91b = xB } rec(91.1, 91, "1284868", ver91b, performance.now()-t91b, "DATE") // 91a: 1710429: very slow cold ~15-20ms, slow 4-5ms let t91a = performance.now(), ver91a try { let t91 = Intl.DateTimeFormat(undefined, {timeZoneName: "longGeneric"}).format(new Date("January 30, 2019 13:00:00")) ver91a = true } catch(e) {} rec(91, 91, "1710429", ver91a, performance.now()-t91a) // 90b: 1520434 // 90+: start offset of Int32Array should be a multiple of 4 // 89-: attempting to construct out-of-bounds TypedArray on ArrayBuffer let t90b = performance.now(), ver90b try {let tst90b = new Int32Array(new ArrayBuffer(4096), 7)} catch(e) {ver90b = (e.message.length == 52)} rec(90.1, 90, "1520434", ver90b, performance.now()-t90b) // 90a: 1537689 let t90a = performance.now(), ver90a try {ver90a = (dom.test90a.defaultValue == "a")} catch(e) {} rec(90, 90, "1537689", ver90a, performance.now()-t90a, "DOM") // 89: 1703213 let t89 = performance.now(), ver89 try { ver89 = ((dom.ctrl89.offsetHeight/dom.test89.offsetHeight) > 0.85) } catch(e) { ver89 = xB } rec(89, 89, "1703213", ver89, performance.now()-t89, "DOM") // 88: 1670124 // FF88+: the escapes \8 and \9 can't be used... // FF87-: no error let t88 = performance.now(), ver88 try {newFn('"use strict" \n"\\8"')} catch(e) {ver88 = true} rec(88, 88, "1670124", ver88, performance.now()-t88, "EVAL") // 87b: 944846 let t87b = performance.now(), ver87b try {ver87b = ((12345).toExponential(3) == 1.235e+4)} catch(e) {} rec(87.1, 87, "944846", ver87b, performance.now()-t87b) // 87a: 1676708 let t87a = performance.now(), ver87a try { let test87a = new Date("Wed Nov 11 2020 19:18:50 GMT+0010 (Central European Standard Time)").toUTCString() ver87a = (test87a !== "Wed, 11 Nov 2020 09:18:50 GMT") } catch(e) {} rec(87, 87, "1676708", ver87a, performance.now()-t87a) // 86: 1685482 // 86+: an expression X in 'for (X of Y)' must not start with 'async of' (64) // 85-: expected '=>' on the same line after an argument list, got '[' (62) let t86 = performance.now(), ver86 try {newFn('for (async of [])')} catch(e) {ver86 = (e.message.length == 64)} rec(86,86, "1685482", ver86, performance.now()-t86, "EVAL" ) //85 possibles? : 1445482 1670062 1635561 // 84: 1673440 // 84+: illegal character U+0040 // 83-: illegal character let t84 = performance.now(), ver84 try {newFn("var x = @")} catch(e) {ver84 = (e.message.length == 24)} rec(84, 84, "1673440", ver84, performance.now()-t84, "EVAL") // 83: 1667094 let t83 = performance.now(), ver83 try { let obj = {exec() {return function(){}}}; let t = RegExp.prototype.test.call(obj,'') ver83 = true } catch(e) {} rec(83.2, 83, "1667094", ver83, performance.now()-t83) // 83a: 1665746: legacy check: toSource (74+): 52 false postive let t83a = performance.now(), ver83a try { let a83 = new Set([1,2,3]); let b83 = new Set([2,3,4]); let c83 = new Set(...a83, ...b83); } catch(e) { ver83a = ("undefined" === typeof Object.toSource && e.message == "1 is not iterable") //debug.push(" 83 1665746 : " + e.name +": "+ e.message) } rec(83.1, 83, "1665746", ver83a, performance.now()-t83a) // 81a: 1657437: legacy check: toSource (74+): 52 false positive // this is unstable in Tor Browser: change from offscreen: use existing visible spans let t81a = performance.now(), ver81a = xF try { dom.test81a.innerHTML = "AB" dom.test81b.innerHTML = "A &#013;B" let w81a = dom.test81a.offsetWidth let w81b = dom.test81b.offsetWidth dom.test81a.textContent = "" dom.test81b.textContent = "" //console.debug(w81a, w81b) ver81a = ("undefined" === typeof Object.toSource && w81a < w81b) } catch(e) { ver81a = xB } rec(81, 81, "1657437", ver81a, performance.now()-t81a, "DOM") // 80: 1651732: legacy check: toSource (74+): 52 false positive let t80 = performance.now(), ver80 try { let obj80 = {[Symbol.toPrimitive]: () => Symbol()} let proxy80 = (new Proxy({},{get: (obj80, prop, proxy80) => prop})) try { for (let i = 0; i < 11; i++) {if (typeof proxy80[obj80] == 'symbol') {}} ver80 = ("undefined" === typeof Object.toSource) } catch(e) {} rec(80.1, 80, "1651732", ver80, performance.now()-t80) } catch(e) { rec(80.1,80, "1651732", xB, performance.now()-t80) } // 79p: 1639246 shipped: javascript.options.weakrefs = true (default) let t79p = performance.now(), ver79p = ("function" === typeof WeakRef) rec(79.4, 79, "1639246", ver79p, performance.now()-t79p, "PREF") // 79a: 1639591 shipped (FF77 added 1629106) // 79+: SyntaxError invalid assignment left-hand side (33) // 78-: SyntaxError expected expression, got '?' (28) let t79a = performance.now(), ver79a try {newFn("let z = (3 ??= 3 * 3)")} catch(e) {ver79a = (e.message.length == 33)} rec(79.3, 79, "1639591", ver79a, performance.now()-t79a, "EVAL") // 79b: 1557718 // ext fuckery: cydec let t79b = performance.now(), ver79b try { ver79b = (new Intl.DateTimeFormat("en", {timeStyle: "short"}).format(new Date()).includes(':')) } catch(e) {ver79b = xB} rec(79.2, 79, "1557718", ver79b, performance.now()-t79b, "INTL") // 79: 1413504 let t79c = performance.now(), ver79c try { ver79c = ("1" !== new Intl.NumberFormat("en", {numberingSystem: "gong"}).format(1)) } catch(e) {ver79c = xB} rec(79.1, 79, "1413504", ver79c, performance.now()-t79c, "INTL") // 79: 1644878: legacy check: toSource (74+): 52 false positive // 79+: entries method called on incompatible boolean // 78-: std_Map_iterator method called on incompatible boolean (54) let t79 = performance.now(), ver79 try {Map.prototype.entries.call(true)} catch(e) { ver79 = ("undefined" === typeof Object.toSource && e.message.length == 45) } rec(79, 79, "1644878", ver79, performance.now()-t79) // 78a: 1589095 let t78a = performance.now(), ver78a try { let test78a = new Intl.ListFormat(undefined,{style: 'long', type: 'unit'}).format(['a','b','c']) ver78a = true } catch(e) {} rec(78.2, 78, "1589095", ver78a, performance.now()-t78a) // 78b: 1634135 let t78b = performance.now(), ver78b try {let regex78b = new RegExp('b'); ver78b = (regex78b.dotAll == false)} catch(e) {ver78b = xB} rec(78.1, 78, "1634135", ver78b, performance.now()-t78b) // 78c: 1633836 let t78c = performance.now(), ver78c try {let test78c = new Intl.NumberFormat(undefined, {style:"unit", unit:"percent"}).format(1/2); ver78c = true} catch(e) {} rec(78, 78, "1633836", ver78c, performance.now()-t78c) // 77: 1627285 let t77 = performance.now(), ver77 = (isNaN(new DOMRect(0, 0, NaN, NaN).top)) rec(77.1, 77, "1627285", ver77, performance.now()-t77) // 77a: 1608168 let t77a = performance.now(), ver77a try {("aa").replaceAll("a","b"); ver77a = true} catch(e) {} rec(77, 77, "1608168", ver77a, performance.now()-t77a) // 75: 1615600 // 75+: 2.5 can't be converted to BigInt because it isn't an integer (60) // 74-: BigInt is not defined (21) let t75 = performance.now(), ver75 try {let test75 = BigInt(2.5)} catch(e) {ver75 = (e.message.length == 60)} rec(75, 75, "1615600", ver75, performance.now()-t75) // 74: 1605835 let t74 = performance.now(), ver74 try {newFn("let t = ({ 1n: 1 })"); ver74 = true} catch(e) {} rec(74, 74, "1605835", ver74, performance.now()-t74, "EVAL") // 73: 1605803 let t73 = performance.now(), ver73 try {ver73 = (getComputedStyle(dom.test73).content == "normal")} catch(e) {} rec(73, 73, "1605803", ver73, performance.now()-t73, "DOM") // 72a: 1441976 let t72a = performance.now(), ver72a try {if (BatteryManager in window) {}} catch(e) {ver72a = true} rec(72.2, 72, "1441976", ver72a, performance.now()-t72a) // 72b: 1566141 let t72b = performance.now(), ver72b try {let test72 = (newFn("null ?? 'foo'")); ver72b = true} catch(e) {} rec(72.1, 72, "1566141", ver72b, performance.now()-t72b, "EVAL") // 72c: 1589072 // 72+: underscore can appear only between digits, not after the last digit in a number (79) // 71-: identifier starts immediately after numeric literal (51) let t72c = performance.now(), ver72c try {newFn('let a = 100_00_;')} catch(e) {ver72c = (e.message.length == 79)} rec(72, 72, "1589072", ver72c, performance.now()-t72c, "EVAL") // 71: 1575980 [charAt[17]] // FF71+: StaticRange requires at least 1 argument, but only 0 were passed // ^ error_fix: StaticRange constructor: At least 1 argument required, but only 0 passed // FF70-: Illegal constructor // FF68-: StaticRange is not defined let t71 = performance.now(), ver71 try { let test71 = new StaticRange() } catch(e) { ver71 = ("function" === typeof StaticRange && (e.message).charAt(0) == "S") // ^ legacy check: 68- StaticRange } rec(71, 71, "1575980", ver71, performance.now()-t71) // 69: 1558387 let t69 = performance.now(), ver69 = ("undefined" === typeof window.DOMError) rec(69, 69, "1558387", ver69, performance.now()-t69) // 68: 1550949 let t68 = performance.now(), ver68 try {ver68 = isNaN(Date.parse("T00:00:00Z"))} catch(e) {ver68 = xB} rec(68, 68, "1550949", ver68, performance.now()-t68, "DATE") // 66: 1514664 let t66 = performance.now(), ver66 = ("function" === typeof TextEncoder.prototype.encodeInto) rec(66, 66, "1514664", ver66, performance.now()-t66) // 65b: 1499026 let t65b = performance.now(), ver65b try {ver65b = ("ia" == Intl.DateTimeFormat.supportedLocalesOf("ia").join())} catch(e) {ver65b = xB} rec(65.1, 65, "1499026", ver65b, performance.now()-t65b, "INTL") // 65a: 1504334 let t65a = performance.now(), ver65a = ("function" === typeof Intl.RelativeTimeFormat) rec(65, 65, "1504334", ver65a, performance.now()-t65a) // 61: 1434007 let t61 = performance.now(), ver61 try {let test61a = (' a').trimStart(); ver61 = true} catch(e) {} rec(61, 61, "1434007", ver61, performance.now()-t61) // 59: 1405993 let t59 = performance.now(), ver59 try {ver59 = ("tt" == Intl.DateTimeFormat.supportedLocalesOf("tt").join())} catch(e) {ver59 = xB} rec(59, 59, "1405993", ver59, performance.now()-t59, "INTL") // 55: 1354974: isFF check: this test hangs blink if (isFF) { let t55 = performance.now(), ver55 try { let maxIndex = Math.pow(2, 31) let list55 = [] list55[maxIndex - 1] = 'a' list55[maxIndex - 0] = 'b' if (list55[maxIndex] !== 'b') { } else if (list55.slice()[maxIndex] !== 'b') { } else if (list55.slice(maxIndex - 1).length !== 2) { } else { ver55 = true } } catch(e) {} rec(55, 55, "1354974", ver55, performance.now()-t55) } else { rec(55, 55, "1354974", xNA, 0) } // 54: 1050755 let t54 = performance.now(), ver54 try { let test54 = [Date.UTC(), Date.UTC(1)] ver54 = (isNaN(test54[1]) == false) } catch(e) {} rec(54, 54, "1050755", ver54, performance.now()-t54) // 53: 1317307 let t53 = performance.now(), ver53 try {Object.defineProperty([], "length", {get(){}})} catch(e) {if (e.name == "TypeError") {ver53 = true}} rec(53, 53, "1317307", ver53, performance.now()-t53) // 52: 837961 let t52 = performance.now(), ver52 try {let test52 = new Intl.DateTimeFormat(undefined, {timeZone: "Europe/Warsaw"}); ver52 = true} catch(e) {} rec(52, 52, "837961", ver52, performance.now()-t52) } run_other() function run_watch() { // watch // 1274354: META // 1439800 // ext fuckery: cydec let t9 = performance.now(), ver9 = xNC let test9 = new Date("11-Nov-11") if (test9.toString() !== "Invalid Date") {ver9 = xOMG} rec(9,"Invalid Date", "1439800", ver9, performance.now()-t9, "DATE") // 1515318: 63 and lower = NaN, 64+ = -2011 let t7 = performance.now(), ver7 = xNC let test7 = new Date("31-Mar-2011").getFullYear() if (test7 !== -2011 && !isNaN(test7)) {ver7 = xOMG + (isFF ? " [" + test7 +"]" : "")} rec(7,"-2011", "1515318", ver7, performance.now()-t7) // 1599375 let t6 = performance.now(), ver6 = xNC let test6 = Date.parse("2019-11-26 07:39:58.286157072 +0000 UTC") if (!isNaN(test6)) {ver6 = xOMG} rec(6,"NaN", "1599375", ver6, performance.now()-t6, "DATE") // 1742592 related let t4 = performance.now(), ver4 = xNC try { let lf4 = new Intl.ListFormat("en") let test4 = "~"+ lf4.format(["", "B"]) +"~" if (test4 !== "~ and B~") {ver4 = xOMG} } catch(e) { ver4 = " "+ zNA + " error " } rec(4,"\" and B\"", "1742592", ver4, performance.now()-t4, "DATE") } } function run() { try {let warmup = Intl.supportedValuesOf("timeZone").includes("America/Coyhaique")} catch(e) {} setTimeout(function() { Promise.all([ get_globals() ]).then(function(){ outputVersion() }) }, 50) } run() </script> </body> </html> ================================================ FILE: tests/windownamea.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=400"> <title>window.name [a]</title> <!-- custom --> <style> :root{ --test0: #b3b3b3; --test7: #9ddc9d; --test12: #9db2dc; --test99: #808080; --bg99: #808080; } body {background-color: #161b22; color: var(--test0);} h2 {color: white; font-size: 14px; text-align: center; margin-top: inherit;} a.blue {color: var(--test12); text-decoration: none;} a.return {color: var(--test12); text-decoration: none; font-size: 14px; line-height: 1.2em} .no_color {color: #b3b3b3;} .mono {font-family: monospace, "Courier New";} .good {color: var(--test7);} .bad {color: #ff6363;} div.nav-title {position: relative;} table { width: 97%; min-width: 380px; max-width: 480px; border-collapse: collapse; margin: 0 auto 10px auto; font-size: 12px; } tbody:before {content: "-"; display: block; line-height: 1em; color: transparent;} td {padding-bottom: 3px; padding-top: 3px; padding-left: 10px;} th {color: black; font-weight: bold; font-size: 16px; padding: 3px 0;} table td:first-child { text-align: right; vertical-align: top;} table td.blurb {text-align: center; line-height: 1.5em;} table td.center {text-align: center;} table td.intro {text-align: left; line-height: 1.5em; padding-bottom: 10px;} #tb99 th {background-color: var(--bg99);} #tb99 td:first-child {color: var(--test99);} .btn {background-color: #161b22; display: inline-block; font-size: 12px; font-family: monospace, "Courier New"; font-weight: bold; padding-left: 6px; padding-right: 6px; cursor: pointer; } .btn0 {border-color: var(--test0); color: var(--test0);} .btn-left {float: left; position: relative; left: -10px; top: 0px;} </style> </head> <body> <table> <tr><td><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb"><a class="return" href="../index.html#other">return to TZP index</a></td></tr> </table> <table id="tb99"> <col width="40%"><col width="60%"> <thead><tr><th colspan="2"><div class="nav-title">window.name [a]</div> </th></tr></thead> <tr> <td><div class="btn-left"><span class="btn0 btn" onClick="run()">[ re-run ]</span></div>current</td> <td class="mono" id="current"></td></tr> <tr><td>setting new</td><td class="mono" id="setnew"></td></tr> <tr><td>check new</td><td class="mono" id="check"></td></tr> <tr><td>next</td> <td><span class="no_color mono">load <a class="blue" href="https://thorin-oakenpants.github.io/testing/windownameb.html">window.name [b]</a></span> </td></tr> </table> <br> <script> 'use strict'; var s0 = " <span class='", sb = s0 +"bad'>", sg = s0 +"good'>", sc = "</span>" function rnd_string(prefix) { return (prefix == undefined ? "" : prefix) + Math.random().toString(36).substring(2, 15) } function run() { let str = "", current = document.getElementById("current"), setnew = document.getElementById("setnew"), check = document.getElementById("check") // clear current.innerHTML = "&nbsp" setnew.innerHTML = "&nbsp" check.innerHTML = "&nbsp" try { str = window.name if (str == undefined) {str = "undefined"} else if (str == "undefined") {str = "\"undefined\""} else if (str == "") {str = "nothing found"} current.innerHTML = str } catch(e) { current.innerHTML = e.name } // create a random string str = rnd_string() var control = str try { window.name = str } catch(e) { str = e.name } setnew.innerHTML = str // read it back: set a little timer setTimeout(function() { let chk = "" try { chk = window.name if (chk == undefined) {chk = "undefined"} else if (chk == "undefined") {chk = "\"undefined\""} else if (chk == "") {chk = "nothing found"} } catch(e) { chk = e.name } check.innerHTML = chk + (chk == control ? sg +"[success]": sb +"[failure]") + sc }, 150) } run() </script> </body> </html> ================================================ FILE: tzp.html ================================================ <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="max-width=800, width=500"> <title>TZP</title> <link rel="preload" href="xml/xmlunstyled.xml" as="script"> <link rel="preload" href="xml/xslterror.xml" as="script"> <link rel="preload" href="images/InvalidImage.png" as="image"> <link rel="preload" href="images/ScaledImage.png" as="image"> <link rel="stylesheet" type="text/css" href="css/index.css"> <link rel="stylesheet" type="text/css" href="css/media.css"> <link rel="stylesheet" href="chrome://global/locale/intl.css"> <!-- required for the parsererror dir leak --> <link rel="stylesheet" type="text/css" href="css/window_size.css"> <link rel="stylesheet" type="text/css" href="css/screen_size.css"> <link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"> <script src="js/globals.js"></script> <script src="js/generic.js"></script> <!--<style></style> important to not have any styleSheet here as when we calculate expected stylesheets for later comparison, these haven't yet been enumerated into document.styleSheets --> </head> <body> <div id="tzpRect"></div> <div id="tzpLV"></div> <div id="tzpSV"></div> <!-- calc must be outside our tzpBody --> <div id="tzpCalc" class="offscreen"><div id="tzpCalcTarget"></div><div class="tzpCalcContainer"></div></div> <!-- keep tzpBody first: contains scrollbar but MUST come after tzpLV and tzpSV in order to be able to be able to select and drag non-overlays --> <div id="tzpBody"> <span translate="no"> <!--offscreen--> <div class="offscreen"> <div id="tzpFS" width=0 height=0></div> <iframe id="tzpInvalidImage" width="100" height="30"></iframe> <iframe id="tzpScaledImage" width="100" height="30"></iframe> <iframe id="tzpXMLunstyled" width="20" height="30"></iframe> <iframe id="tzpXSLT" width="40" height="30"></iframe> <iframe id="tzpIframe" width="0" height="0"></iframe> <audio id="tzpAudio"></audio><video id="tzpVideo" width="20px"></video> <div id="tzpDiv"></div> <div id="tzpDocFont" style="font-family: 'test font name'"></div> <div id="tzpSVG"></div> <div id="tzpScroll"><div></div></div> <span id="tzpScript"></span> <svg id="tzpSwitch" width="0px" height="0px" viewBox="0 0 0 0"></svg> <div id='tzpGraphite' style="font-family: 'graphite';"><span>+</span><span>-</span></div> <div id='tzpDPI' style='height: 1in; width: 1in;'></div> <img id="tzpBrand" src="chrome://branding/content/about-wordmark.svg"> <img id="tzpAbout" src="about:logo"> <!--for async fallback--> <div> <span> &#x007F; </span><span> &#x0218; </span><span> &#x058F; </span><span> &#x05C6; </span> <span> &#x061C; </span><span> &#x0700; </span><span> &#x08E4; </span><span> &#x097F; </span> <span> &#x09B3; </span><span> &#x0B82; </span><span> &#x0D02; </span><span> &#x10A0; </span> <span> &#x115A; </span><span> &#x17DD; </span><span> &#x1950; </span><span> &#x1C50; </span> <span> &#x1CDA; </span><span> &#x1D790; </span><span> &#x1E9E; </span><span> &#x20B0; </span> <span> &#x20B8; </span><span> &#x20B9; </span><span> &#x20BA; </span><span> &#x20BD; </span> <span> &#x20E3; </span><span> &#x21E4; </span><span> &#x23AE; </span><span> &#x2425; </span> <span> &#x2581; </span><span> &#x2619; </span><span> &#x2B06; </span><span> &#x2C7B; </span> <span> &#x302E; </span><span> &#x3095; </span><span> &#x532D; </span><span> &#x6E2F; </span> <span> &#xA73D; </span><span> &#xA830; </span><span> &#xF003; </span><span> &#xF810; </span> <span> &#xFBEE; </span><span> &#xFFF9; </span><span> &#xFFFD; </span><span> &#xFFFF; </span> </div> <div id="tzpFontMax" class="normalized"></div> <div id='tzpDirection'></div> </div> <!--hidden--> <div class="hidden"> <div><img id="tzpBrandHidden" src="chrome://branding/content/about-wordmark.svg"></div> <!--<div><img id="tzpBrandHidden" src="images/about-wordmark.svg"></div>--> <canvas id="tzpCanvasGet"></canvas> <canvas id="tzpCanvasGetSolid"></canvas> <canvas id="tzpCanvasPath"></canvas> <canvas id="tzpCanvasTo"></canvas> <canvas id="tzpCanvasToSolid"></canvas> <div class="normalized"><canvas id="tzpTextmetrics"></canvas></div> <span id="tzpColor"></span> <div id="tzpDPR" style="border: 0.1px solid;">foo</div> <div id="tzpWidget" class="normalized"> <input type="button" id="tzpbutton"> <input type="checkbox"> <input type="color"> <input type="date"> <input type="datetime-local"> <input type="email"> <input type="file"> <input type="hidden"> <input type="image"> <input type="month" id="tzpmonth"> <input type="number"> <input type="password"> <input type="radio"> <input type="range"> <input type="reset"> <input type="search" id="tzpsearch"> <select id="tzpselect"></select> <input type="submit"> <input type="tel"> <input type="text"> <textarea id="tzptextarea"></textarea> <input type="time"> <input type="url"> <input type="week" id="tzpweek"> </div> <div> <a id="tzpClrlink" href="fake" class="">unvisited</a> <a id="tzpClrvisited-link" href="" class="">visited</a> </div> </div> <table> <tr><td><a name="top"></a><h2>TorZillaPrint</h2></td></tr> <tr><td class="blurb" id="index"><a id="tzpLink" class="return" href="index.html">return to TZP index</a></td></tr> </table> <span id="tzpContent"> <!--FPs--> <table id="tbfp"> <col width="27%"><col width="73%"> <thead><tr><th colspan="2"><a name="finger"></a> <div class="nav-title"><a href="#finger">loose <span class="perf"><sup>1</sup></span> fingerprints</a> <div class="nav-up"><a href="#top">&#9650;</a> <span class="c perf" id="perfAll"></span></div> <div class="nav-down"><a href="#screend">&#9660;</a></div></div> </th></tr></thead> <tr><td> <div class="btn-left"><span class="btn btn0" onClick="outputSection(`all`)">[ re-run ]</span></div> prototype | proxy <sup>2</sup></td> <td class="mono"> <span class="c" id="protohash"></span> <div class="btn-right c" id="protohealth"></div> </td></tr> <tr><td>document</td><td class="mono"> <span class="c" id="documenthash"></span> <span class="c" id="documentbtns"></span> <div class="btn-right c" id="documenthealth"></div> </td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><span class="normal"><span class="no_color">fingerprints are always <a target="_blank" class="blue" href="https://github.com/arkenfox/TZP/#-fingerprints-are-always-loose">loose</a> <sup>1</sup>, prototype/proxy lies by <a target="_blank" class="blue" href="https://github.com/abrahamjuliot/creepjs">CreepJS</a> <sup>2</sup>, json format by <a target="_blank" class="blue" href="https://github.com/lydell/json-stringify-pretty-compact">Simon Lydell</a> </span> </span></td></tr> </table> <!--screen--> <table id="tb1"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="screen"></a> <div class="nav-title"><a href="#screen">screen</a> <div class="nav-up"><a href="#finger">&#9650;</a> <span class="c perf" id="perfscreen"></span></div> <div class="nav-down"><a href="#uad">&#9660;</a></div> <div class="nav-right"><a name="screend"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn1 btn" onClick="outputSection(1)">[ re-run ]</span></div> <span class="c" id="screenhash"></span> <div class="btn-right"></div> </td></tr> <!--position--> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">availLeft<br>availTop<br>left<br>top</span></div> &nbsp; screen</td> <td class="mono"><span class="c" id="position_screen"></span> <!-- used to calculate overlayCharLen --> <div class="btn-right s99">positions</div></td></tr> <tr><td> <div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">mozInnerScreenX<br>mozInnerScreenY<br>screenX<br>screenY</span></div> &nbsp; window</td><td class="mono border-bottom"><span class="c" id="position_window"></span> </td></tr> <!--device orientation--> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">type<br>angle<br>orientation + aspect-ratio</span></div> &nbsp; [device] orientation</td> <td class="mono"> <span class="c" id="orientation_device_summary"></span> <span class="c" id="orientation_device"></span> <div class="btn-right s99"><span id="labelS" class="btn btnright" onClick="togglerows('S','btn')">[+]</span></div> </td> </tr> <tr class="togS"><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">mozOrientation<br>orientation.angle<br>orientation.type</span></div> &nbsp; device</td> <td class="mono"> <span class="c" id="orientation_mozOrientation"></span> | <span class="c" id="orientation_orientation.angle"></span> | <span class="c" id="orientation_orientation.type"></span> <div class="btn-right s99">orientation</div> </td></tr> <tr class="togS"><td>[iframe] device</td> <td class="mono"> <span class="c" id="orientation_mozOrientation_iframe"></span> | <span class="c" id="orientation_orientation.angle_iframe"></span> | <span class="c" id="orientation_orientation.type_iframe"></span> </td></tr> <tr class="togS"> <td><div class="ttip"><span class="icon">[ i ]</span><span class="ttxtb">-moz-device-orientation<br> device-aspect-ratio</span></div> &nbsp; [css] device</td> <td class="mono"><span id="cssOm"></span> | <span id="cssDAR"></span></td> </tr> <tr class="togS"> <td><div class="ttip"><span class="icon">[ i ]</span><span class="ttxtb">-moz-device-orientation<br> device-aspect-ratio</span></div> &nbsp; [matchMedia] device</td> <td class="mono border-bottom"> <span class="c" id="orientation_-moz-device-orientation"></span> | <span class="c" id="orientation_device-aspect-ratio"></span> </td> </tr> <!--screen--> <tr><td>screen</td><td class="mono"> <span class="c" id="screen_summary"></span> <span class="c" id="sizes_screen"></span> <span class="c" id="screen_aspect_ratio"></span> </td></tr> <tr class="togS"><td> <div class="ttip"><span class="icon">[ i ]</span><span class="ttxt cssrange">range 400-2560</span></div> &nbsp; [min-device-] css <sup>1</sup></td> <td class="mono"><span id="S"></span><div class="btn-right s99">screen</div></td> </tr> <tr class="togS"><td>iframe</td><td class="c mono" id="screen_iframe"></td></tr> <tr class="togS"><td>matchMedia <sup>2</sup></td><td class="c mono" id="screen_media"></td></tr> <tr class="togS"><td>screen</td><td class="mono border-bottom"><span class="c" id="screen_screen"></span></td></tr> <!--available--> <tr><td>available</td><td class="mono"> <span class="c" id="available_summary"></span> <span class="c" id="sizes_available"></span> <span class="c" id="size_dock"></span> </td></tr> <tr class="togS"><td>iframe</td><td class="mono"> <span class="c" id="available_iframe"></span> <div class="btn-right s99">available</div> <span class="c" id="screen_sizes"></span> </td></tr> <tr class="togS"><td>screen</td><td class="mono border-bottom"> <span class="c" id="available_screen"></span> </td></tr> <!--outer--> <tr class="togA"><td>initial outer</td><td class="mono" id="initial_outer"></td></tr> <tr><td>outer</td><td class="mono"> <span class="c" id="outer_summary"></span> <span class="c" id="sizes_outer"></span> <span class="c" id="size_chrome"></span> </td></tr> <tr class="togS"><td>iframe</td><td class="mono"> <span class="c" id="outer_iframe"></span> <div class="btn-right s99">outer</div> </td></tr> <tr class="togS"><td>window</td><td class="mono border-bottom"> <span class="c" id="outer_window"></span> <span class="c" id="window_sizes"></span> </td></tr> <!--window orientation--> <tr class="togS"> <td><div class="ttip"><span class="icon">[ i ]</span><span class="ttxt">aspect-ratio<br>orientation</span></div> &nbsp; [css] window</td> <td class="mono"><span id="cssAR"></span> | <span id="cssO"></span><div class="btn-right s99">orientation</div> </td></tr> <tr class="togS"> <td><div class="ttip"><span class="icon">[ i ]</span><span class="ttxt">aspect-ratio<br>orientation</span> </div> &nbsp; [matchMedia] window</td> <td class="mono border-bottom"> <span class="c" id="orientation_aspect-ratio"></span> | <span class="c" id="orientation_orientation"></span> </td></tr> <!--inner--> <tr class="togA"><td>initial inner</td><td class="c mono" id="initial_inner"></td></tr> <tr><td>inner</td><td class="mono"> <span class="c" id="inner_summary"></span> <span class="c" id="sizes_inner"></span> <span class="c" id="size_newwin"></span> </td></tr> <tr class="togS"><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt cssrange">range 400-2560</span></div> &nbsp; [min-] css <sup>1, 3</sup></td> <td class="mono"><span id="D"></span> <div class="btn-right s99">inner</div></td></tr> <tr id="A1" class="hidden"><td>document</td><td class="c mono" id="inner_document"></td></tr> <tr class="togS"><td>matchMedia <sup>2</sup></td><td class="c mono" id="inner_media"></td></tr> <tr class="togS"><td>[small viewport] sv*</td><td class="c mono" id="inner_viewport"></td></tr> <tr class="A2 togS"><td>window</td> <td class="mono border-bottom"> <span class="c" id="inner_window"></span><span class="c" id="dynamic_note"></span> </td></tr> <!--viewport--> <tr class="togA"><td>[large viewport] lv*</td><td class="mono border-top"> <span id="viewport_large"></span> <div class="btn-right s99">viewport</div> </td></tr> <tr class="togA"><td>dynamic toolbar</td><td class="c mono" id="dynamic_toolbar"></td></tr> <tr class="A2"><td>viewport</td><td class="mono"> <span class="c" id="viewport_summary"></span> <span class="c" id="sizes_viewport"></span> </td></tr> <tr class="A2 togS"><td>document</td><td class="mono"> <span class="c" id="viewport_document"></span> <div class="btn-right s99">viewport</div> </td></tr> <tr class="A2 togS"><td>element <sup>4</sup></td><td class="c mono" id="viewport_element"></td></tr> <tr class="A2 togS"><td>visualViewport</td><td class="c mono" id="viewport_visualViewport"></td></tr> <!--other--> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">[css] display-mode<br>display-mode<br>fullScreen<br>mozFullScreenEnabled</span></div> &nbsp; display-mode | fullscreen</td> <td class="mono border-top"><span id="cssDM"></span> | <span class="c" id="display-mode"></span> | <span class="c" id="windowfullScreen"></span> | <span class="c" id="mozFullScreenEnabled"></span></td></tr> <tr><td><span class="btn btn0" onClick="outputUser('fullscreenElement')">[ run ]</span> fullscreenElement <sup>4</sup></td> <td class="mono"> <span class="c" id="fullscreenElement"></span> <div id="btnFS" class="btn-right btn1" onClick="exitUserFS()">[ exit ]</div> </td></tr> <tr><td><span class="mono no_color">[F11]</span> &nbsp; fullscreen</td><td class="c mono" id="fsSize"></td></tr> <tr><td><span class="btn btn0" onClick="outputUser('newwin')">[ run ]</span> <div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">attempts to open a new blank<br>window as big as possible<br>and grab the dimensions</span></div> &nbsp; new window</td><td class="mono border-bottom"><span class="c" id="newwin"></span></td></tr> <!-- pixels--> <tr><td>[div] dpi</td><td class="mono"><span class="c" id="dpi_div"></span> <div class="btn-right s99">pixels</div></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span><span class="ttxt">range 40-400</span></div> &nbsp; [css min-resolution] dpi</td> <td class="mono"><span id="P"></span><span class="cssc" id="pixels_dpi_css"></span> <div class="btn-right c" id="pixels_match"></div> </td></tr> <tr><td>[matchMedia] dpi</td><td class="mono"><span class="c" id="pixels_dpi"></span></td></tr> <tr><td>[matchMedia] dppx | dpcm</td><td class="mono"> <span class="c" id="pixels_dppx"></span> | <span class="c" id="pixels_dpcm"></span> </td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">-moz-device-pixel-ratio<br>-webkit-device-pixel-ratio<br></span></div> &nbsp; *device-pixel-ratio</td><td class="mono"> <span class="c" id="pixels_-moz-device-pixel-ratio"></span> | <span class="c" id="pixels_-webkit-device-pixel-ratio"></span> </td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span><span class="ttxtb">window<br>iframe</span></div> &nbsp; devicePixelRatio</td><td class="mono"> <span class="c" id="pixels_devicePixelRatio"></span> | <span class="c" id="pixels_devicePixelRatio_iframe"></span> </td></tr> <tr><td>[border] devicePixelRatio</td><td class="mono"><span class="c" id="devicePixelRatio_border"></span></td></tr> <tr class="A2"><td>visualViewport scale</td><td class="c mono" id="visualViewport_scale"></td></tr> <tr><td colspan="2"></td></tr> <!--space--> <tr><td colspan="2"><span class="normal"><span class="no_color">code based on work by </span><a target="_blank" class="blue" href="https://arthuredelstein.github.io/tordemos/media-query-fingerprint.html">Arthur Edelstein</a> <sup>1</sup> <span class="no_color">, </span> <a target="_blank" class="blue" href="https://canvasblocker.kkapsner.de/test">kkapsner</a><span class="no_color"> & </span> <a target="_blank" class="blue" href="https://github.com/kkapsner/CanvasBlocker">CanvasBlocker</a> <sup>2</sup> <span class="no_color">, </span> <a target="_blank" class="blue" href="https://blog.pastly.net/posts/2016-09-04-how-css-alone-can-help-track-you/">Matt Traudt</a> <sup>3</sup> <span class="no_color"> and </span> <a target="_blank" class="blue" href="https://github.com/earthlng/testpages">earthlng</a> <sup>4</sup> </span></td></tr> </table> <!--ua--> <table id="tb2"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="ua"></a> <div class="nav-title"><a href="#ua">agent</a> <div class="nav-up"><a href="#screen">&#9650;</a> <span class="c perf" id="perfagent"></span></div> <div class="nav-down"><a href="#featured">&#9660;</a></div> <div class="nav-right"><a name="uad"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn2 btn" onClick="outputSection(2)">[ re-run ]</span></div> <span class="c" id="agenthash"></span> <div class="btn-right"></div> </td></tr> <!--agent*--> <tr><td><span id="labelUA" class="btn btn0" onClick="togglerows('UA','btn')">[+]</span> userAgent</td><td class="c mono" id="useragent"></td></tr> <tr class="togUA"><td>appCodeName</td><td class="c mono spaces" id="useragent_appCodeName"></td></tr> <tr class="togUA"><td>appName</td><td class="c mono spaces" id="useragent_appName"></td></tr> <tr class="togUA"><td>appVersion</td><td class="c mono spaces" id="useragent_appVersion"></td></tr> <tr class="togUA"><td>buildID</td><td class="c mono spaces" id="useragent_buildID"></td></tr> <tr class="togUA"><td>oscpu</td><td class="c mono spaces" id="useragent_oscpu"></td></tr> <tr class="togUA"><td>platform</td><td class="c mono spaces" id="useragent_platform"></td></tr> <tr class="togUA"><td>product</td><td class="c mono spaces" id="useragent_product"></td></tr> <tr class="togUA"><td>productSub</td><td class="c mono spaces" id="useragent_productSub"></td></tr> <tr class="togUA"><td>userAgent</td><td class="c mono spaces" id="useragent_userAgent"></td></tr> <tr class="togUA"><td>vendor</td><td class="c mono spaces" id="useragent_vendor"></td></tr> <tr class="togUA"><td>vendorSub</td><td class="c mono spaces" id="useragent_vendorSub"></td></tr> <tr><td> <!--<span id="labelUAD" class="btn btn0" onClick="togglerows('UAD','btn')">[+]</span>--> userAgentData</td><td class="c mono" id="useragentdata"></td></tr> <!--<tr class="togUAD"><td>coming soon</td><td class="c mono" id="uad_item1"></td></tr>--> <tr><td colspan="2" class="center">------</td></tr> <tr><td><span id="labelAI" class="btn btn0" onClick="togglerows('AI','btn')">[+]</span> iframes <sup>1</sup></td><td class="c mono" id="uaIframes"></td></tr> <tr class="togAI"><td>[contentWindow] document root</td><td class="c mono" id="agent_content_docroot"></td></tr> <tr class="togAI"><td>[contentWindow] with URL</td><td class="c mono" id="agent_content_with_url"></td></tr> <tr class="togAI"><td>[window] document root</td><td class="c mono" id="agent_window_docroot"></td></tr> <tr class="togAI"><td>[window] with URL</td><td class="c mono" id="agent_window_with_url"></td></tr> <tr class="togAI"><td>iframe access</td><td class="c mono" id="agent_iframe_access"></td></tr> <tr class="togAI"><td>nested</td><td class="c mono" id="agent_nested"></td></tr> <tr class="togAI"><td>window access</td><td class="c mono" id="agent_window_access"></td></tr> <tr><td><span id="labelAW" class="btn btn0" onClick="togglerows('AW','btn')">[+]</span> workers</td><td class="mono s99" id="uaWorkers">summary not coded</td></tr> <tr class="togAW"><td>worker</td><td class="c mono" id="agent_worker"></td></tr> <tr class="togAW"><td>shared worker</td><td class="c mono" id="agent_worker_shared"></td></tr> <tr class="togAW"><td>service worker</td><td class="c mono" id="agent_worker_service"></td></tr> <tr><td><span class="btn btn0" onClick="outputUser('agent_open')">[ run ]</span> window.open</td><td class="c mono" id="agent_open"></td></tr> <tr><td colspan="3"><span class="normal"><span class="no_color">iframe code based on work by </span> <a target="_blank" class="blue" href="https://canvasblocker.kkapsner.de/test">kkapsner</a><span class="no_color"> & </span> <a target="_blank" class="blue" href="https://github.com/abrahamjuliot/creepjs">CreepJS</a> <sup>1</sup> </span></td></tr> </table> <!--fd--> <table id="tb3"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="feature"></a> <div class="nav-title"><a href="#feature">feature detection</a> <div class="nav-up"><a href="#ua">&#9650;</a> <span class="c perf" id="perffeature"></span></div> <div class="nav-down"><a href="#regiond">&#9660;</a></div> <div class="nav-right"><a name="featured"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="intro"><span class="normal"> <span class="no_color">These tests are to show that you cannot hide your engine <a class="blue" target="blank" href="tests/engineprop.html">[PoC1]</a> + <a class="blue" target="blank" href="tests/engine.html">[PoC2]</a>, version <a class="blue" target="blank" href="tests/versions.html">[PoC]</a> or OS <a class="blue" target="blank" href="tests/os.html">[PoC]</a>.</span> </span></td></tr> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn3 btn" onClick="outputSection(3)">[ re-run ]</span></div> <span class="c" id="featurehash"></span> <div class="btn-right"></div> </td></tr> <tr><td>[css] branding</td><td class="mono" id="tzpWordmark"></td></tr> <tr><td>[css] browser</td><td class="mono" id="tzpResource"></td></tr> <tr><td>[browser] architecture</td><td class="c mono" id="browser_architecture"></td></tr> <tr><td>[&infin;] architecture</td><td class="c mono" id="infinity_architecture"></td></tr> <tr><td>browser</td><td class="mono"> <span class="c" id="browser"></span> | <span class="c" id="logo"></span> | <span class="c" id="wordmark"></span> </td></tr> <tr><td>version</td><td class="c mono" id="version"></td></tr> <tr><td>os</td><td class="c mono" id="os"></td></tr> <tr><td>[css] os <sup>1</sup></td> <td><span style="font-family: Arimo, DejaVu Serif, ABR;">Linux</span><span style="font-family: Lucida Grande, ABR;">Mac</span><span style="font-family: Segoe UI, ABR;">Windows</span><span style="font-family: Roboto, Droid Sans, ABR;">Android</span> </td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><span class="normal"> <span class="no_color">code based on work by </span><a target="_blank" class="blue" href="https://arthuredelstein.github.io/tordemos/os-detection-font-css.html">Arthur Edelstein</a> <sup>1</sup> </span></td></tr> </table> <!--region--> <table id="tb4"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="region"></a> <div class="nav-title"><a href="#region">region</a> <div class="nav-up"><a href="#feature">&#9650;</a> <span class="c perf" id="perfregion"></span></div> <div class="nav-down"><a href="#headersd">&#9660;</a></div> <div class="nav-right"><a name="regiond"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn4 btn" onClick="outputSection(4)">[ re-run ]</span></div> <span class="c" id="regionhash"></span> <div class="btn-right"></div> </td></tr> <!--geo--> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">navigator<br>window</span></div> &nbsp; geolocation</td><td class="mono border-bottom"> <span class="c" id="geolocation_navigator"></span> | <span class="c" id="geolocation"></span> <div class="btn-right s99">geo</div> </td></tr> <!--lang--> <tr><td>language</td><td class="c mono" id="language"></td></tr> <tr><td>languages</td><td class="mono"> <span class="c" id="languages"></span> <div class="btn-right s99">languages</div></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">lowercase + sorted</span></div> &nbsp; [system] languages</td> <td class="mono border-bottom"><span class="c" id="languages_system"></span></td></tr> <!--locale--> <tr><td>locale</td><td class="c mono" id="locale"></td></tr> <tr><td>[intl] locale</td><td class="mono"> <span class="c" id="locale_intl"></span> <div class="btn-right s99">intl</div></td></tr> <tr><td>[tolocalstring] locale</td><td class="mono"> <span class="c" id="locale_tolocalestring"></span> <span class="c" id="locale_tolocalestring_matches_intl"></span> <div class="btn-right" id="intl_data"></div> </td></tr> <tr><td>[intl] dates</td><td class="mono"> <span clas="c" id="dates_intl"></span> <div class="btn-right" id="intl_perf"></div> </td></tr> <tr><td>[to*string] dates</td><td class="mono border-bottom"> <span class="c" id="dates_to*string"></span><span class="c" id="dates_to*string_matches_intl"></span> </td></tr> <!--l10n--> <!--<tr><td>css</td><td class="c mono" id="l10n_css"></td></tr>--> <tr><td>[parsererror] direction</td> <td class="c mono" id="l10n_parsererror_direction"> </td></tr> <tr><td>media messages</td><td class="mono"> <span class="c" id="l10n_media_messages"></span> <div class="btn-right s99">l10n</div> </td></tr> <tr><td>reporting messages</td><td class="c mono" id="l10n_reporting_messages"></td></tr> <tr><td>validation messages</td><td class="c mono" id="l10n_validation_messages"></td></tr> <tr><td>XML messages</td><td class="c mono" id="l10n_xml_messages"></td></tr> <tr><td>XML prettyprint</td><td class="c mono" id="l10n_xml_prettyprint"></td></tr> <tr><td>XSLT messages</td><td class="c mono" id="l10n_xslt_messages"></td></tr> <tr><td>XSLT sort</td><td class="mono border-bottom"><span class="c" id="l10n_xslt_sort"></span></td></tr> <!--tz--> <tr><td><span id="labelTT" class="btn btn0" onClick="togglerows('TT','btn')">[+]</span> timezone</td><td class="c mono" id="timezone"></td></tr> <tr class="togTT"><td>timeZone</td><td class="c mono" id="timezone_timeZone"></td></tr> <tr class="togTT"><td>timeZoneId</td><td class="c mono" id="timezone_timeZoneId"></td></tr> <tr class="togTT"><td>zonedDateTimeISO</td><td class="c mono" id="timezone_zonedDateTimeISO"></td></tr> <!--tz offset--> <tr><td><span id="labelTL" class="btn btn0" onClick="togglerows('TL','btn')">[+]</span> [offset] timezone</td> <td class="mono"><span class="c" id="timezone_offset"></span> <div class="btn-right s99">timezone</div> </td></tr> <!--date--> <tr class="togTL"><td>plainDateISO</td><td class="mono"> <span class="c" id="timezone_offset_plainDateISO"></span> <div class="btn-right-inset s99 border-top">date</div> </td></tr> <tr class="togTL"><td>toDateString</td><td class="c mono" id="timezone_offset_toDateString"></td></tr> <!--time--> <tr class="togTL"><td>plainTimeISO</td><td class="mono"> <span class="c spaces" id="plainTimeISOspaces"></span> <span class="c" id="timezone_offset_plainTimeISO"></span> <div class="btn-right-inset s99 border-top">time</div> </td></tr> <tr class="togTL"><td>toTimeString</td><td class="mono"> <span class="c spaces" id="toTimeStringspaces"></span> <span class="c" id="timezone_offset_toTimeString"><span> </td></tr> <!--datetime--> <tr class="togTL"><td>plainDateTimeISO</td><td class="mono"> <span class="c" id="timezone_offset_plainDateTimeISO"></span> <div class="btn-right-inset s99 border-top">datetime</div> </td></tr> <tr class="togTL"><td>timeZone</td><td class="c mono" id="timezone_offset_timeZone"></td></tr> <tr class="togTL"><td>toLocaleDateString</td><td class="c mono" id="timezone_offset_toLocaleDateString"></td></tr> <tr class="togTL"><td>toLocaleString</td><td class="c mono" id="timezone_offset_toLocaleString"></td></tr> <tr class="togTL"><td>toLocaleTimeString</td><td class="c mono" id="timezone_offset_toLocaleTimeString"></td></tr> <tr class="togTL"><td>toString</td><td class="c mono" id="timezone_offset_toString"></td></tr> <tr class="togTL"><td>zonedDateTimeISO</td><td class="c mono" id="timezone_offset_zonedDateTimeISO"></td></tr> <!--lastmod--> <tr class="togTL"><td>iframe</td><td class="mono"> <span class="c" id="timezone_offset_iframe"></span> <div class="btn-right-inset s99 border-top">lastModified</div> </td></tr> <tr class="togTL"><td>parseFromString</td><td class="c mono" id="timezone_offset_parseFromString"></td></tr> <tr class="togTL"><td>parseHTMLUnsafe</td><td class="c mono" id="timezone_offset_parseHTMLUnsafe"></td></tr> <tr class="togTL"><td>EXSLT</td><td class="c mono" id="timezone_offset_exslt"></td></tr> <!--tz offsets--> <tr><td><span id="labelTO" class="btn btn0" onClick="togglerows('TO','btn')">[+]</span>[offsets] timezone</td> <td class="mono"><div class="btn-right c" id="timezone_offsets_data"></div> <span class="c" id="timezone_offsets"></span></td></tr> <tr class="togTO"><td>[get] components</td><td class="c mono" id="timezone_offsets_components"></td></tr> <tr class="togTO"><td>[getUTC] components</td><td class="c mono" id="timezone_offsets_components_utc"></td></tr> <tr class="togTO"><td>date</td><td class="c mono" id="timezone_offsets_date"></td></tr> <tr class="togTO"><td><a class="blue" href="tests/timezones.html" target="blank">[PoC]</a> &nbsp; date.parse</td><td class="c mono" id="timezone_offsets_date.parse"></td></tr> <tr class="togTO"><td>date.valueOf</td><td class="c mono" id="timezone_offsets_date.valueOf"></td></tr> <tr class="togTO"><td>getTime</td><td class="c mono" id="timezone_offsets_getTime"></td></tr> <tr class="togTO"><td>getTimezoneOffset</td><td class="c mono" id="timezone_offsets_getTimezoneOffset"></td></tr> <tr class="togTO"><td>offsetNanoseconds</td><td class="c mono" id="timezone_offsets_offsetNanoseconds"></td></tr> <tr class="togTO"><td>Symbol.toPrimitive</td><td class="c mono" id="timezone_offsets_Symbol.toPrimitive"></td></tr> <tr class="togTO"><td>timeZoneName</td><td class="c mono" id="timezone_offsets_timeZoneName"></td></tr> <!--details--> <tr><td colspan="2" class="showhide"> <span id="labelL" class="btnb" onClick="togglerows('L')">&#9660; show details</span></td></tr> <!--date/time + formatting--> <tr class="togL"><td></td><td class="s4">[to*string] dates</td></tr> <tr class="togL"><td>toTimeString</td><td class="c" id="ldt1"></td></tr> <tr class="togL"><td>date/time</td><td class="c" id="ldt2"></td></tr> <tr class="togL"><td>toString</td><td class="c" id="ldt3"></td></tr> <!--options--> <tr class="togL"><td>toLocaleString</td><td class="c" id="ldt4"></td></tr> <tr class="togL"><td>toLocaleDateString</td><td class="c" id="ldt5"></td></tr> <tr class="togL"><td>toLocaleTimeString</td><td class="c" id="ldt6"></td></tr> <!--no options--> <tr class="togL"><td>toLocaleString</td><td class="c" id="ldt7"></td></tr> <tr class="togL"><td>[Typed Array] toLocaleString</td><td class="c" id="ldt8"></td></tr> <tr class="togL"><td>toLocaleDateString</td><td class="c" id="ldt9"></td></tr> <tr class="togL"><td>toLocaleTimeString</td><td class="c" id="ldt10"></td></tr> <!--Intl.DateTimeFormat--> <tr class="togL"><td></td><td class="s4">[intl] dates</td></tr> <tr class="togL"><td>DateTimeFormat</td><td class="c" id="ldt11"></td></tr> <tr class="togL"><td>[formatToParts] DateTimeFormat</td><td class="c" id="ldt12"></td></tr> <tr class="togL"><td>DateTimeFormat</td><td class="c" id="ldt13"></td></tr> <tr class="togL"><td>timeZoneNames</td><td class="c" id="ldt14"></td></tr> <tr class="togL"><td>formatrange</td><td class="c" id="ldt15"></td></tr> <tr class="togL" colspan="2"><td></td></tr> </table> <!--headers--> <table id="tb5"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="headers"></a> <div class="nav-title"><a href="#headers">headers</a> <div class="nav-up"><a href="#region">&#9650;</a> <span class="c perf" id="perfheaders"></span></div> <div class="nav-down"><a href="#storaged">&#9660;</a></div> <div class="nav-right"><a name="headersd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn5 btn" onClick="outputSection(5)">[ re-run ]</span></div> <span class="c" id="headershash"></span> <div class="btn-right"></div> </td></tr> <tr><td>connection</td><td class="c mono" id="connection"></td></tr> <tr><td>doNotTrack</td><td class="c mono" id="doNotTrack"></td></tr> <tr><td>globalPrivacyControl</td><td class="c mono" id="globalPrivacyControl"></td></tr> <tr><td>onLine</td><td class="c mono" id="onLine"></td></tr> <tr><td colspan="2"></td></tr> </table> <!--storage--> <table id="tb6"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="storage"></a> <div class="nav-title"><a href="#storage">cookies & storage</a> <div class="nav-up"><a href="#headers">&#9650;</a> <span class="c perf" id="perfstorage"></span></div> <div class="nav-down"><a href="#devicesd">&#9660;</a></div> <div class="nav-right"><a name="storaged"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn6 btn" onClick="outputSection(6)">[ re-run ]</span></div> <span class="c" id="storagehash"></span> <div class="btn-right"></div> </td></tr> <tr><td>caches</td><td class="c mono" id="caches"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">session | persistent<br>tests are JS 1st party</span></div> &nbsp; cookies</td><td class="c mono" id="ctest"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">dom.cookieStore.enabled</span></div> &nbsp; cookieStore</td><td class="c mono" id="cstest"></td></tr> <tr><td>localStorage</td><td class="c mono" id="localStorage"></td></tr> <tr><td>sessionStorage</td><td class="c mono" id="sessionStorage"></td></tr> <tr><td>indexedDB</td><td class="mono"><span class="c" id="indexedDB"></span> | <span class="c" id="indexedDB_test"></span></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">service | shared | worker</span></div> &nbsp; workers</td><td class="c mono" id="workers"> </td></tr> <tr><td>[tests] workers</td><td class="mono"> <span class="c" id="worker_service_test"></span> | <span class="c" id="worker_shared_test"></span> | <span class="c" id="worker_test"></span> </td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span><span class="ttxt">dom.fs.enabled</span></div> &nbsp; file system</td><td class="c mono" id="filesystem"></td></tr> <tr><td><span class="btn btn0" onClick="outputUser('storage_manager')">[ run ]</span> storage manager</td><td class="c mono" id="storage_manager"></td></tr> <tr><td>storage quota</td><td class="c mono" id="storage_quota"></td></tr> <tr><td colspan="2"></td></tr> </table> <!--devices--> <table id="tb7"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="devices"></a> <div class="nav-title"><a href="#devices">devices & hardware</a> <div class="nav-up"><a href="#storage">&#9650;</a> <span class="c perf" id="perfdevices"></span></div> <div class="nav-down"><a href="#svgd">&#9660;</a></div> <div class="nav-right"><a name="devicesd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn7 btn" onClick="outputSection(7)">[ re-run ]</span></div> <span class="c" id="deviceshash"></span> <div class="btn-right"></div> </td></tr> <tr><td>battery</td><td class="mono c" id="battery"></td></tr> <tr><td>[pixel | color] depth</td> <td class="mono"><span class="c" id="pixelDepth"></span> | <span class="c" id="colorDepth"></span></td></tr> <tr><td>deviceMemory</td><td class="c mono" id="deviceMemory"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtx">[css | matchmedia] device-posture<br>navigator.devicePosture</span></div> &nbsp; devicePosture</td><td class="mono"> <span id="cssDP"></span> | <span class="c" id="devicePosture_device-posture"></span> | <span class="c" id="devicePosture_devicePosture"></span> </td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtx">dom.security.featurePolicy.webidl.enabled</span></div> &nbsp; featurePolicy</td><td class="c mono" id="featurePolicy"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">dom.maxHardwareConcurrency</span></div> &nbsp; hardwareConcurrency</td><td class="c mono" id="hardwareConcurrency"></td></tr> <tr><td>keyboard</td><td class="c mono" id="keyboard"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtx">media.navigator.enabled<br>media.peerconnection.enabled</span></div> &nbsp; media devices</td><td class="c mono" id="mediaDevices"></td></tr> <tr><td>[constraints] media devices</td><td class="c mono" id="mediaDevices_constraints"></td></tr> <tr><td>memory</td><td class="c mono" id="memory"></td></tr> <tr><td>permissions</td><td class="c mono" id="permissions"></td></tr> <tr><td id="tzpPointer"><span class="btn btn0">[ run ]</span> &nbsp; pointer event <sup>1</sup></td><td class="c mono" id="pointer_event"></td></tr> <tr><td>recursion</td><td class="c mono" id="recursion"></td></tr> <tr><td>screen.isExtended</td><td class="c mono" id="screen_isextended"></td></tr> <tr><td>touch</td><td class="c mono" id="touch"></td></tr> <tr><td>viewport-segments</td><td class="mono"><span id="cssVS"></span> <span class="cssc" id="viewport-segments_css"></span> | <span class="c" id="viewport-segments"></span></td></tr> <tr><td colspan="2"><span class="normal"><span class="no_color">pointer code based on work by </span> <a target="_blank" class="blue" href="https://patrickhlauke.github.io/touch/">Patrick Lauke</a> <sup>1</sup> </span></td></tr> </table> <!--svg--> <table id="tb8"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="svg"></a> <div class="nav-title"><a href="#svg">svg</a> <div class="nav-up"><a href="#devices">&#9650;</a> <span class="c perf" id="perfsvg"></span></div> <div class="nav-down"><a href="#canvasd">&#9660;</a></div> <div class="nav-right"><a name="svgd"></a></div></div> </th></tr></thead> <tr><td>test</td><td class="mono">result</td></tr> </table> <!--canvas--> <table id="tb9"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="canvas"></a> <div class="nav-title"><a href="#canvas">canvas</a> <div class="nav-up"><a href="#svg">&#9650;</a> <span class="c perf" id="perfcanvas"></span></div> <div class="nav-down"><a href="#webgld">&#9660;</a></div> <div class="nav-right"><a name="canvasd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="intro"><span class="normal"><span class="no_color">These tests are only checking for protection, not entropy. Additional canvas tests [iframes, workers, offscreen] can be found at <a target="blank" class="blue" href="https://canvasblocker.kkapsner.de/test/test.html">CanvasBlocker</a></span> </span></td></tr> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn9 btn" onClick="outputSection(9)">[ re-run ]</span></div> <span class="c" id="canvashash"></span> <div class="btn-right"></div> </td></tr> <tr><td><a class="blue" href="tests/canvasnoise.html" target="blank">[PoC]</a> &nbsp; <div class="ttip"><span class="icon">[ i ]</span><span class="ttxt">random per run</span></div> &nbsp; getImageData</td><td class="c mono" id="getImageData"></td></tr> <tr><td>[solid] getImageData</td><td class="c mono" id="getImageData_solid"></td></tr> <tr><td>isPointInPath</td><td class="c mono" id="isPointInPath"></td></tr> <tr><td>isPointInStroke</td><td class="c mono" id="isPointInStroke"></td></tr> <tr><td>toBlob</td><td class="c mono" id="toBlob"></td></tr> <tr><td>[solid] toBlob</td><td class="c mono" id="toBlob_solid"></td></tr> <tr><td><a class="blue" href="tests/canvasrfp.html" target="blank">[PoC]</a> &nbsp; toDataURL</td><td class="c mono" id="toDataURL"></td></tr> <tr><td>[solid] toDataURL</td><td class="c mono" id="toDataURL_solid"></td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><span class="normal"> <span class="no_color">canvas code based on work by </span> <a target="_blank" class="blue" href="https://canvasblocker.kkapsner.de/test">kkapsner</a><span class="no_color"> & </span> <a target="_blank" class="blue" href="https://github.com/kkapsner/CanvasBlocker">CanvasBlocker</a> </span></td></tr> </table> <!--webgl--> <table id="tb10"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="webgl"></a> <div class="nav-title"><a href="#webgl">webgl</a> <div class="nav-up"><a href="#canvas">&#9650;</a> <span class="c perf" id="perfwebgl"></span></div> <div class="nav-down"><a href="#audiod">&#9660;</a></div> <div class="nav-right"><a name="webgld"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn10 btn" onClick="outputSection(10)">[ re-run ]</span></div> <span class="c" id="webglhash"></span> <div class="btn-right"></div> </td></tr> <tr><td>experimental</td><td class="mono">result</td></tr> <tr><td>webgl</td><td class="mono">result</td></tr> <tr><td>webgl2</td><td class="mono">result</td></tr> <tr><td colspan="3"><span class="normal"><span class="no_color">webgl code by </span> <a target="_blank" class="blue" href="https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2">Abraham Juliot</a> </span></td></tr> </table> <!--audio--> <table id="tb11"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="audio"></a> <div class="nav-title"><a href="#audio">audio</a> <div class="nav-up"><a href="#webgl">&#9650;</a> <span class="c perf" id="perfaudio"></span></div> <div class="nav-down"><a href="#fontsd">&#9660;</a></div> <div class="nav-right"><a name="audiod"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn11 btn" onClick="outputSection(11)">[ re-run ]</span></div> <span class="c" id="audiohash"></span> <div class="btn-right"></div> </td></tr> <tr><td>audioContext <sup>1</sup></td><td class="c mono" id="audioContext"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">copyFromChannel<br>getChannelData<br>sum of buffer</span></div> &nbsp; offlineAudioContext <sup>2</sup></td><td class="c mono" id="offlineAudioContext"></td></tr> <tr><td colspan="2" class="center">------</td></tr> <tr><td><span class="btn btn0" onClick="outputUser('audio_test')">[ run ]</span> hash</td><td class="gc mono" id="audio_test"></td></tr> <tr><td>OscillatorNode <sup>1</sup></td><td class="uaudio_test gc mono" id="audio_test_oscillator"></td></tr> <tr><td>+DynamicsCompressor <sup>1</sup></td><td class="uaudio_test gc mono" id="audio_test_oscillator_compressor"></td></tr> <tr><td colspan="2"></td></tr> <tr><td colspan="2"><span class="normal"><span class="no_color">audio code based on work by </span> <a target="_blank" class="blue" href="https://audiofingerprint.openwpm.com/">openWPM</a> <sup>1</sup> <span class="no_color"> and </span> <a target="_blank" class="blue" href="https://canvasblocker.kkapsner.de/test">kkapsner</a><span class="no_color"> & </span> <a target="_blank" class="blue" href="https://github.com/kkapsner/CanvasBlocker">CanvasBlocker</a> <sup>2</sup> </span></td></tr> </table> <!--fonts--> <table id="tb12"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="fonts"></a> <div class="nav-title"><a href="#fonts">fonts</a> <div class="nav-up"><a href="#audio">&#9650;</a> <span class="c perf" id="perffonts"></span></div> <div class="nav-down"><a href="#codecsd">&#9660;</a></div> <div class="nav-right"><a name="fontsd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn12 btn" onClick="outputSection(12)">[ re-run ]</span></div> <span class="c" id="fontshash"></span> <div class="btn-right smaller"><span class="c" id="fntBtn"></span></div> </td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtx">browser.display.use_document_fonts</span></div> &nbsp; document fonts</td><td class="c mono" id="document_fonts"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">layout.css.font-tech.enabled</span></div> &nbsp; font-format | font-tech</td><td class="mono"><span class="c" id="font-format"></span> | <span class="c" id="font-tech"></span> </td></tr> <!--fonts--> <tr><td>[faces] fonts</td><td class="mono border-top"> <div class="btn-right c" id="font_detection"></div><span id="font_faces"><span></td></tr> <tr><td>[offscreen] fonts</td><td class="mono c" id="font_offscreen"></td></tr> <tr><td><span id="labelFS" class="btn btn0" onClick="togglerows('FS','btn')">[+]</span> [sizes | names] fonts <sup>1</sup></td> <td class="mono"><span class="c" id="font_sizes"></span> | <span class="c" id="font_names"></span></td> </tr> <tr class="togFS"><td>client</td><td class="c mono" id="font_sizes_client"></td></tr> <tr class="togFS"><td>offset</td><td class="c mono" id="font_sizes_offset"></td></tr> <tr class="togFS"><td>scroll</td><td class="c mono border-bottom" id="font_sizes_scroll"></td></tr> <tr class="togFS"><td>pixel</td><td class="c mono" id="font_sizes_pixel"></td></tr> <tr class="togFS"><td>pixelsize</td><td class="c mono border-bottom" id="font_sizes_pixelsize"></td></tr> <tr class="togFS"><td>perspective</td><td class="c mono" id="font_sizes_perspective"></td></tr> <tr class="togFS"><td>transform</td><td class="c mono border-bottom" id="font_sizes_transform"></td></tr> <tr class="togFS"><td>[domrect] bounding</td><td class="c mono" id="font_sizes_domrectbounding"></td></tr> <tr class="togFS"><td>bounding range</td><td class="c mono" id="font_sizes_domrectboundingrange"></td></tr> <tr class="togFS"><td>client</td><td class="c mono" id="font_sizes_domrectclient"></td></tr> <tr class="togFS"><td>client range</td><td class="c mono border-bottom" id="font_sizes_domrectclientrange"></td></tr> <tr><td>[base sizes] fonts <sup>1</sup></td><td class=" mono border-top"> <span class="c" id="font_sizes_base"></span> | <span class="c" id="font_sizes_base_reported"></span> </td></tr> <tr><td>[maximum sizes] fonts</td><td class="c mono" id="font_sizes_max"></td></tr> <tr><td>[methods] fonts <sup>1</sup></td><td class="c mono" id="font_sizes_methods"></td></tr> <tr><td>[moz] fonts</td><td class="c mono" id="fonts_moz"></td></tr> <tr><td>[system] fonts</td><td class="c mono" id="fonts_system"></td></tr> <tr><td>[widget] fonts</td><td class="c mono" id="fonts_widget"></td></tr> <tr><td><a class="blue" href="tests/fontasync.html" target="blank">[PoC]</a> <span id="labelFG" class="btn btn0" onClick="togglerows('FG','btn')">[+]</span> glyphs <sup>2</sup></td><td class="c mono" id="glyphs"></td></tr> <tr class="togFG"><td>glyphs</td><td class="spaces smaller" id="glyphs_visual"></td></tr> <tr><td>[test] graphite <sup>3</sup></td><td class="c mono" id="graphite"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">proportional font<br>monospace size<br>sans-serif size<br>serif size</span></div> &nbsp; script defaults</td><td class="c mono" id="script_defaults"></td></tr> <!--tm--> <tr><td>actualboundingbox | baseline</td><td class="mono"> <span class="c spaces" id="textmetrics_actualboundingbox"></span> | <span class="c" id="textmetrics_baseline"></span> </td></tr> <tr><td>emheight | fontboundingbox</td><td class="mono"> <span class="c spaces" id="textmetrics_emheight"></span> | <span class="c" id="textmetrics_fontboundingbox"></span> </td></tr> <!--<tr><td>[measureText] width <sup>4</sup></td><td class="c mono" id="textmetrics_width"></td></tr>--> <tr><td>[css | test] woff2 <sup>4</sup></td><td class="mono"> <span id="cssWoff2"></span> | <span class="c" id="woff2"></span></td></tr> <tr><td colspan="2"></td></tr> <!--creds--> <tr><td colspan="2"><span class="normal"> <span class="no_color">custom font from </span> <a target="_blank" class="blue" href="https://graphite.sil.org/">SIL</a> <sup>3</sup> <span class="no_color"> and code based on work by </span> <a target="_blank" class="blue" href="https://github.com/abrahamjuliot/creepjs">CreepJS</a> <sup>1</sup> <span class="no_color">, </span> <a target="_blank" class="blue" href="https://www.bamsoftware.com/talks/fc15-fontfp/fontfp.html#demo">David Fifield & Serge Egelman</a> <sup>2</sup> <span class="no_color"> and </span> <a target="_blank" class="blue" href="https://github.com/filamentgroup/woff2-feature-test">Filament Group</a> <sup>4</sup> </span></td></tr> </table> <!--codecs--> <table id="tb13"> <col width="31%"><col width="69%"> <thead><tr><th colspan="3"><a name="codecs"></a> <div class="nav-title"><a href="#codecs">codecs</a> <div class="nav-up"><a href="#fonts">&#9650;</a> <span class="c perf" id="perfcodecs"></span></div> <div class="nav-down"><a href="#cssd">&#9660;</a></div> <div class="nav-right"><a name="codecsd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn13 btn" onClick="outputSection(13)">[ re-run ]</span></div> <span class="c" id="codecshash"></span> <div class="btn-right smaller"><span id="mediaBtn"></span></div></td></tr> <tr><td>autoplaypolicy</td><td class="c mono" id="getAutoplayPolicy"></td></tr> <tr><td>[user] autoplaypolicy</td><td class="c mono" id="getAutoplayPolicy_user"></td></tr> <tr><td>EME</td><td class="c mono" id="eme"></td></tr> <tr><td>canPlayType</td><td class="mono"> <span class="c" id="audio_canPlayType"></span> | <span class="c" id="video_canPlayType"></span></td></tr> <tr><td>[RTC] getCapabilities</td><td class="mono"> <span class="c" id="audio_getCapabilities_rtc"></span> | <span class="c" id="video_getCapabilities_rtc"></span></td></tr> <tr><td>isTypeSupported</td><td class="mono"> <span class="c" id="audio_isTypeSupported"></span> | <span class="c" id="video_isTypeSupported"></span></td></tr> <tr><td>preload</td><td class="c mono" id="preload_htmlmediaelement"></td></tr> <tr><td colspan="2"></td></tr> </table> <!--css--> <table id="tb14"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="css"></a> <div class="nav-title"><a href="#css">css</a> <div class="nav-up"><a href="#codecs">&#9650;</a> <span class="c perf" id="perfcss"></span></div> <div class="nav-down"><a href="#elementsd">&#9660;</a></div> <div class="nav-right"><a name="cssd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn14 btn" onClick="outputSection(14)">[ re-run ]</span></div> <span class="c" id="csshash"></span> <div class="btn-right"></div> </td></tr> <tr><td>[css4] colors</td><td class="c mono" id="colors_css4"></td></tr> <tr><td>[deprecated] colors</td><td class="c mono" id="colors_deprecated"></td></tr> <tr><td><a class="blue" href="tests/csscolors.html" target="blank">[PoC]</a> &nbsp; [-moz-] colors</td><td class="c mono" id="colors_moz"></td></tr> <tr><td><span id="labelCS" class="btn btn0" onClick="togglerows('CS','btn')">[+]</span> computed styles <sup>1</sup></td><td class="c mono" id="computed_styles"></td></tr> <tr class="togCS"><td>CSSRuleList.style <sup>1</sup></td><td class="c mono" id="computed_styles_cssrulelist"></td></tr> <tr class="togCS"><td>DOMParser <sup>1</sup></td><td class="c mono" id="computed_styles_domparser"></td></tr> <tr class="togCS"><td>getComputedStyle <sup>1</sup></td><td class="c mono" id="computed_styles_getcomputed"></td></tr> <tr class="togCS"><td>HTMLElement.style <sup>1</sup></td><td class="c mono" id="computed_styles_htmlelement"></td></tr> <!--media: matchmedia + @media--> <tr><td><span id="labelMM" class="btn btn0" onClick="togglerows('MM','btn')">[+]</span> media</td><td class="c mono" id="media"></td></tr> <tr class="togMM"><td>color</td><td class="mono"><span id="cssC"></span> <span class="cssc" id="media_color_css"></span> | <span class="c" id="media_color"></span></td></tr> <tr class="togMM"><td>color-gamut</td><td class="mono"><span id="cssCG"></span> <span class="cssc" id="media_color-gamut_css"></span> | <span class="c" id="media_color-gamut"></span></td></tr> <tr class="togMM"><td>dynamic-range</td><td class="mono"><span id="cssDR"></span> <span class="cssc" id="media_dynamic-range_css"></span> | <span class="c" id="media_dynamic-range"></span></td></tr> <tr class="togMM"><td>forced-colors</td><td class="mono"><span id="cssFC"></span> <span class="cssc" id="media_forced-colors_css"></span> | <span class="c" id="media_forced-colors"></span></td></tr> <tr class="togMM"><td>any-hover</td><td class="mono"><span id="cssAH"></span> <span class="cssc" id="media_any-hover_css"></span> | <span class="c" id="media_any-hover"></span></td></tr> <tr class="togMM"><td>hover</td><td class="mono"><span id="cssH"></span> <span class="cssc" id="media_hover_css"></span> | <span class="c" id="media_hover"></span></td></tr> <tr class="togMM"><td>inverted-colors</td><td class="mono"><span id="cssIC"></span> <span class="cssc" id="media_inverted-colors_css"></span> | <span class="c" id="media_inverted-colors"></span></td></tr> <tr class="togMM"><td>any-pointer</td><td class="mono"><span id="cssAP"></span> <span class="cssc" id="media_any-pointer_css"></span> | <span class="c" id="media_any-pointer"></span></td></tr> <tr class="togMM"><td>pointer</td><td class="mono"><span id="cssP"></span> <span class="cssc" id="media_pointer_css"></span> | <span class="c" id="media_pointer"></span></td></tr> <tr class="togMM"><td>prefers-color-scheme</td><td class="mono"><span id="cssPCS"></span> <span class="cssc" id="media_prefers-color-scheme_css"></span> | <span class="c" id="media_prefers-color-scheme"></span></td></tr> <tr class="togMM"><td>prefers-contrast</td><td class="mono"><span id="cssPC"></span> <span class="cssc" id="media_prefers-contrast_css"></span> | <span class="c" id="media_prefers-contrast"></span></td></tr> <tr class="togMM"><td>prefers-reduced-data</td><td class="mono"><span id="cssPRD"></span> <span class="cssc" id="media_prefers-reduced-data_css"></span> | <span class="c" id="media_prefers-reduced-data"></span></td></tr> <tr class="togMM"><td>prefers-reduced-motion</td><td class="mono"><span id="cssPRM"></span> <span class="cssc" id="media_prefers-reduced-motion_css"></span> | <span class="c" id="media_prefers-reduced-motion"></span></td></tr> <tr class="togMM"><td>prefers-reduced-transparency</td><td class="mono"><span id="cssPRT"></span> <span class="cssc" id="media_prefers-reduced-transparency_css"></span> | <span class="c" id="media_prefers-reduced-transparency"></span></td></tr> <tr class="togMM"><td>update</td><td class="mono"><span id="cssUD"></span> <span class="cssc" id="media_update_css"></span> | <span class="c" id="media_update"></span></td></tr> <tr class="togMM"><td>video-dynamic-range</td><td class="mono"><span id="cssVDR"></span> <span class="cssc" id="media_video-dynamic-range_css"></span> | <span class="c" id="media_video-dynamic-range"></span></td></tr> <tr><td>site colors</td><td class="c mono" id="site_colors"></td></tr> <tr><td>site styles</td><td class="c mono" id="site_styles"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtx">layout.css.always_underline_links</span></div> &nbsp; underline links</td><td class="c mono" id="underline_links"></td></tr> <tr><td colspan="3"><span class="normal"><span class="no_color">code by </span> <a target="_blank" class="blue" href="https://github.com/abrahamjuliot/creepjs">CreepJS</a> <sup>1</sup> </span></td></tr> </table> <!--elements--> <table id="tb15"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="elements"></a> <div class="nav-title"> <a href="#elements">elements</a> <div class="nav-up"><a href="#css">&#9650;</a> <span class="c perf" id="perfelements"></span></div> <div class="nav-down"><a href="#ciphersd">&#9660;</a></div> <div class="nav-right"><a name="elementsd"></a></div> </div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn15 btn" onClick="outputSection(15)">[ re-run ]</span></div> <span class="c" id="elementshash"></span> <div class="btn-right"></div> </td></tr> <tr><td><a class="blue" href="tests/domrectspoof.html" target="blank">[PoC]</a> &nbsp; DOMRect</td><td class="c mono" id="domrect"></td></tr> <tr><td>HTMLElement keys <sup>1</sup></td><td class="c mono" id="htmlelement_keys"></td></tr> <tr><td><a class="blue" href="tests/elementfont.html" target="blank">[PoC]</a> &nbsp; font</td><td class="c mono" id="element_font"></td></tr> <tr><td><a class="blue" href="tests/elementforms.html" target="blank">[PoC]</a> &nbsp; forms</td><td class="c mono" id="element_forms"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">mathml.disabled</span></div> &nbsp; mathml</td><td class="c mono" id="element_mathml"></td></tr> <tr><td><a class="blue" href="tests/elementother.html" target="blank">[PoC]</a> &nbsp; other</td><td class="c mono" id="element_other"></td></tr> <tr><td>[auto | thin] scrollbars</td><td class="c mono" id="element_scrollbars"></td></tr> <tr><td colspan="2"><span class="no_color">code by </span><a target="_blank" class="blue" href="https://github.com/abrahamjuliot/creepjs">CreepJS</a> <sup>1</sup> </td></tr> </table> <!--ciphers--> <table id="tb16"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="ciphers"></a> <div class="nav-title"><a href="#ciphers">ciphers / tls</a> <div class="nav-up"><a href="#elements">&#9650;</a></div> <div class="nav-down"><a href="#timingd">&#9660;</a></div> <div class="nav-right"><a name="ciphersd"></a></div></div> </th></tr></thead> <tr><td>test</td><td class="mono">result</td></tr> </table> <!--timing--> <table id="tb17"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="timing"></a> <div class="nav-title"><a href="#timing">timing</a> <div class="nav-up"><a href="#ciphers">&#9650;</a> <span class="c perf" id="perftiming"></span></div> <div class="nav-down"><a href="#miscd">&#9660;</a></div> <div class="nav-right"><a name="timingd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn99 btn" style="cursor: default;">[ re-run ]</span></div> <span class="c" id="timinghash"></span> <div class="btn-right"></div> </td></tr> <!--manual--> <tr><td><span class="btn btn0" onClick="outputUser('timing_audio')">[ run ]</span> <span id="labelTA" class="btn btn0" onClick="togglerows('TA','btn')">[+]</span> audio</td><td class="c mono" id="timing_audio"></td></tr> <tr class="togTA"><td>contexttime</td><td class="c mono utiming_audio" id="timing_audio_contexttime"></td></tr> <tr class="togTA"><td>performancetime</td><td class="c mono utiming_audio" id="timing_audio_performancetime"></td></tr> <!--auto--> <tr><td><span id="labelTP" class="btn btn0" onClick="togglerows('TP','btn')">[+]</span> timing precision</td><td class="c mono" id="timing_precision"></td></tr> <tr class="togTP"><td>currenttime</td><td class="c mono" id="timing_precision_currenttime"></td></tr> <tr class="togTP"><td>date</td><td class="c mono" id="timing_precision_date"></td></tr> <tr class="togTP"><td>EXSLT</td><td class="c mono" id="timing_precision_exslt"></td></tr> <tr class="togTP"><td> [temporal] instant</td><td class="c mono" id="timing_precision_instant"></td></tr> <tr class="togTP"><td>mark</td><td class="c mono" id="timing_precision_mark"></td></tr> <tr class="togTP"><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtx">dom.enable_performance_navigation_timing</span></div> &nbsp; navigation</td><td class="c mono" id="timing_precision_navigation"></td></tr> <tr class="togTP"><td>now</td><td class="c mono" id="timing_precision_now"></td></tr> <tr class="togTP"><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">dom.enable_performance</span></div> &nbsp; performance</td><td class="c mono" id="timing_precision_performance"></td></tr> <tr class="togTP"><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">privacy*reduceTimerPrecision</span></div> &nbsp; reducetimer</td><td class="c mono" id="timing_precision_reducetimer"></td></tr> <tr class="togTP"><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">dom.enable_resource_timing</span></div> &nbsp; resource</td><td class="c mono" id="timing_precision_resource"></td></tr> <tr class="togTP"><td>timestamp</td><td class="c mono" id="timing_precision_timestamp"></td></tr> </table> <!--misc--> <table id="tb18"> <col width="31%"><col width="69%"> <thead><tr><th colspan="2"><a name="misc"></a> <div class="nav-title"><a href="#misc">miscellaneous</a> <div class="nav-up"><a href="#timing">&#9650;</a> <span class="c perf" id="perfmisc"></span></div> <div class="nav-down"><a href="#theend">&#9660;</a></div> <div class="nav-right"><a name="miscd"></a></div></div> </th></tr></thead> <tr><td colspan="2" class="secthash"> <div class="btn-left"><span class="btn18 btn" onClick="outputSection(18)">[ re-run ]</span></div> <span class="c" id="mischash"></span> <div class="btn-right"></div> </td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">dom.use_components_shim</span></div> &nbsp; component interfaces</td><td class="c mono" id="component_interfaces"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtx">Firefox 74+ : javascript.options.<br>property_error_message_fix</span></div> &nbsp; error message fix</td><td class="c mono" id="error_message_fix"></td></tr> <tr><td>[css] math</td><td class="c mono" id="math_css"></td></tr> <tr><td>[other] math</td><td class="c mono" id="math_other"></td></tr> <tr><td>[trigonometric] math</td><td class="c mono" id="math_trig"></td></tr> <tr><td>navigator keys</td><td class="c mono" id="navigator_keys"></td></tr> <tr><td>pdf</td><td class="c mono" id="pdf"></td></tr> <tr><td>speech engines</td><td class="c mono" id="speech_engines"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxt">svg.disabled</span></div> &nbsp; svg</td><td class="c mono" id="svg_enabled"></td></tr> <tr><td><div class="ttip"><span class="icon">[ i ]</span> <span class="ttxtb">javascript.options.wasm</span></div> &nbsp; wasm</td><td class="c mono" id="wasm"></td></tr> <tr><td>webdriver</td><td class="c mono" id="webdriver"></td></tr> <tr><td>window functions</td><td class="c mono" id="window_functions"></td></tr> <tr><td>window properties <sup>1</sup></td> <td class="mono"><span class="c" id="window_properties"></span><span class="c" id="consolestatus"></span></td></tr> <tr><td>[tampered] window properties <sup>1</sup></td> <td class="c mono" id="window_properties_tampered"></td></tr> <tr><td colspan="2"><span class="normal"><span class="no_color">code by </span> <a target="_blank" class="blue" href="https://github.com/abrahamjuliot/creepjs">CreepJS</a> <sup>1</sup> </span></td></tr> </table> <!--perf--> <table id="tbperf"> <col width="1%"><col width="10%"><col width="89%"> <thead><tr><th colspan="3" class="showhide"> <span id="labelP" class="btnb" onClick="togglerows('P','perf & debugging')">&#9660; show perf & debugging</span></th></tr></thead> <tr class="togP"><td colspan="2">global perf <br><span class="btnb mono no_color" onClick="output_perf('all',true)">[<span id="perfGBtn">more</span>]</span> </td><td class="mono spaces gc" id="perfG"></td></tr> <tr class="togP"><td colspan="2">click perf <br><span class="btnb mono no_color" onClick="output_perf('x',true)">[<span id="perfSBtn">more</span>]</span> </td><td colspan="2" class="mono spaces gc" id="perfS"></td></tr> </table> <a name="theend" id="theend"></a><br> </span> <!--end tzpcontent--> <div id='blockmsg' style="display: none;"></div><!--block--> <!--overlay--> <div id="modaloverlay" onClick="metricsAction('close')"></div> <div id="overlay" class='mono'> <div id="overlaytop"> <span id="metricsTitle"></span> <span id="overlaybuttons"> <span class='btn0 btnc' onClick="metricsAction('download')" id="metricDownload">[DOWNLOAD]</span> <span class='btn0 btnc' onClick="metricsAction('console')" id="metricsConsole">[CONSOLE]</span> <span class='btn0 btnc' onClick="copyclip('metricsDisplay')" id="metricsBtnCopy"><abbr title="Ctrl+C">[COPY]</abbr></span> <span class='btn0 btnc' onClick="metricsAction('close')"><abbr title="Esc">[CLOSE]</abbr></span> <br><br> <div id="metricOptions"><br><p><u>FORMAT</u></p> <p id="optDetail">detail <input type="radio" name="optOverlay" id="optFormat_detail" value="_detail" checked onchange="metricsAction()"></p> <p>summary <input type="radio" name="optOverlay" id="optFormat_summary" value="_summary" onchange="metricsAction()"></p> <p id="optFlat">flat <input type="radio" name="optOverlay" id="optFormat_flat" value="_flat" onchange="metricsAction()"></p> <p id="optList">list <input type="radio" name="optOverlay" id="optFormat_list" value="_list" onchange="metricsAction()"></p> <span id="groupHealth">–––<p>all <input type="radio" name="optHealth" checked id="healthAll" onchange="metricsAction()"></p> <p><span id="overlay_tick" class="good">✓ </span><input type="radio" name="optHealth" id="healthPass" onchange="metricsAction()"></p> <p><span id="overlay_cross" class="bad">✗ </span><input type="radio" name="optHealth" id="healthFail" onchange="metricsAction()"></p> </span> </div> </span> </div> <div id="overlaycontent"> <span id="metricsDisplay" class="spaces"></span> <br><br><div id="overlayInfo" class='faint spaces'></div> </div> <div id="overlaykit" class="hidden"></div> </div> </span> <!-- end translate --> </div> <!-- end tzpBody --> <script src="js/prototypeLies.js"></script> <script src="js/region.js"></script> <script src="js/fonts.js"></script> <script src="js/screen.js"></script> <script src="js/storage.js"></script> <script src="js/devices.js"></script> <script src="js/audio.js"></script> <script src="js/css.js"></script> <script src="js/elements.js"></script> <script src="js/misc.js"></script> <script src="js/codecs.js"></script> <script src="js/canvas.js"></script> <script src="js/webgl.js"></script> <script src="js/iframes.js"></script> <script src="js/user.js"></script> </body> </html> ================================================ FILE: tzpiframe.html ================================================ <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>TZP iframed</title> <style> body {background-color: white; color: black;} .vwh { height: 100vh; width: 100vw; position: fixed; top: 0; left: 0; border: 0px; } </style> </head> <body> <iframe id="iframe" class="vwh"></iframe> <script> 'use strict'; try { let iframe = document.getElementById('iframe') iframe.src='tzp.html' } catch(e) {console.log(e)} </script> </body> </html> ================================================ FILE: xml/xmlunstyled.xml ================================================ <?xml version="1.0" encoding="UTF-8"?><a>a</a> ================================================ FILE: xml/xslterror.xml ================================================ <?xml version="1.0"?><?xml-stylesheet type="text/xsl" href=""?><a>a</a>