├── .gitignore
├── package.json
├── README.md
├── LICENSE.md
├── style.css
├── index.html
├── gosh-hang-it.js
└── lib
└── polyfill.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gosh-hang-it",
3 | "version": "1.0.0",
4 | "description": "Polyfill for hanging-punctuation CSS property",
5 | "main": "gosh-hang-it.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/liamdanger/gosh-hang-it.git"
12 | },
13 | "keywords": [
14 | "punctuation",
15 | "typography",
16 | "polyfill"
17 | ],
18 | "author": "Liam Campbell",
19 | "license": "ISC",
20 | "bugs": {
21 | "url": "https://github.com/liamdanger/gosh-hang-it/issues"
22 | },
23 | "homepage": "https://github.com/liamdanger/gosh-hang-it#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gosh Hang It!
2 |
3 | Gosh Hang It is a simple polyfill for the [`hanging-punctuation`](https://css-tricks.com/almanac/properties/h/hanging-punctuation/) CSS property, which exists in the spec but is not supported by any major browser.
4 |
5 | Here is an [example](http://liamdanger.github.io/gosh-hang-it)!
6 |
7 | ## Usage
8 | Gosh Hang It has one dependency, and that's `polyfill.js`. It's included in the repo! All you have to do is:
9 |
10 | 1. Add `polyfill.js` and `gosh-hang-it.js` to your HTML
11 | 2. Apply the CSS property `hanging-punctuation: first;` to your stylesheets liberally
12 | 3. Rejoice :balloon:
13 |
14 | ---
15 |
16 | 
17 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Liam Campbell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | /**
3 | So first off we've gotta make sure we're in a nice space, plenty of room
4 | to maneuver for the trick we're about to pull.
5 | */
6 | margin: 0 auto;
7 | max-width: 33em;
8 | padding: 20px;
9 |
10 | /**
11 | And this whole thing doesn't get pulled off without text, so we better
12 | make sure we've got some fine, nice-looking text to read.
13 | */
14 | color: #222;
15 | font-family: "Droid Serif", "Georgia", "Times New Roman", serif;
16 | font-size: 20px;
17 | line-height: 1.5;
18 | }
19 |
20 | /**
21 | But I mean, we don't want the text to take away from the main attraction!
22 | Let's put a couple of little rules next to the text, so you can tell what's
23 | gonna happen. In the business, we call this "telegraphing".
24 | */
25 | p {
26 | border-left: 1px solid #e0e0ff;
27 | }
28 |
29 | /**
30 | Don't forget, we're pulling off several versions of this little show, so we
31 | need to keep 'em separated.
32 | */
33 | p + p {
34 | margin-top: 28px;
35 | }
36 |
37 | /**
38 | At some point we might want to display some code samples. They should look nice!
39 | */
40 | code {
41 | color: #666;
42 | font-size: 16px;
43 |
44 | padding: 2px 2px;
45 |
46 | background-color: #eee;
47 | border-radius: 2px;
48 | }
49 |
50 | /**
51 | Let's also make sure links look okay
52 | */
53 | a {
54 | color: #222;
55 | text-decoration: none;
56 | box-shadow: 0 5px 0 #D3E8F6;
57 | }
58 | a:hover {
59 | color: #268BD2;
60 | }
61 |
62 | /**
63 | Finally, here's the main attraction. It might look like crapnasty unsupported
64 | CSS to you now, but just wait and see if you don't change your mind.
65 | */
66 |
67 | .hang-none { hanging-punctuation: none; }
68 | .hang-first { hanging-punctuation: first; }
69 | .hang-last { hanging-punctuation: last; }
70 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Gosh Hang It is a simple polyfill for the hanging-punctuation CSS property, which exists in the spec but is not supported by any major browser. The R. Buckminster Fuller quotes below are inside a container that has the hanging-punctuation: first rule applied to them.
18 |
19 |
20 |
21 | “I am enthusiastic over humanity’s extraordinary and sometimes ‘very’ timely ingenuities. If
22 | you are in a shipwreck and all the boats are gone, a piano top buoyant enough to keep you
23 | afloat that comes along makes a fortuitous life preserver. But this is not to say that the best
24 | way to design a life preserver is in the form of a piano top. I think that we are clinging to a
25 | great many piano tops in accepting yesterday’s fortuitous contrivings as constituting the only
26 | means for solving a given problem. Our brains deal exclusively with special-case experiences.
27 | Only our minds are able to discover the “generalized” principles operating without exception in
28 | each and every special-experience case which if detected and mastered will give knowledgeable
29 | advantage in all instances.”
30 |
31 |
32 |
33 | “Because our spontaneous initiative has been frustrated, too often inadvertently, in earliest
34 | childhood we do not tend, customarily, to dare to think competently regarding our potentials.
35 | We find it socially easier to go on with our narrow, shortsighted specializations and leave it to
36 | others—primarily to the politicians—to find some way of resolving our common dilemmas.
37 | Countering that spontaneous grownup trend to narrowness I will do my, hopefully ‘childish,’
38 | best to confront as many of our problems as possible by employing the longest-distance
39 | thinking of which I am capable—though that may not take us very far into the future.”
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/gosh-hang-it.js:
--------------------------------------------------------------------------------
1 | (function(window, document, undefined) {
2 |
3 | // Enable strict mode; no shirking or loafing permitted
4 | "use strict";
5 |
6 | // Array of hangable punctuation characters
7 | var hangables = ['\'', '"', '‘', '’', '“', '”', ',', '.', '،', '۔', '、', '。', ',', '.', '﹐', '﹑', '﹒', '。', '、', '«', '»'];
8 |
9 | // Don't wrap characters in tags that contain non-display text
10 | var disallowedNodes = ['title', 'head', 'script', 'style'];
11 |
12 | // Monolithic objecty thing
13 | var gosh = {};
14 |
15 | // Array of Hangable objects will live here
16 | gosh.chars = [];
17 |
18 | gosh.wrapHangables = function(el) {
19 | // Wrap hangable characters in an easily queryable and styleable span
20 |
21 | // but first make sure that we're not after a non-hangable node
22 | if (disallowedNodes.indexOf(el.nodeName.toLowerCase()) != -1) {
23 | return false;
24 | }
25 |
26 | var nodes = el.childNodes;
27 |
28 | for (var i = 0; i < nodes.length; ++i) {
29 | var node = nodes[i];
30 |
31 | if(node.nodeType == 1) {
32 | // Recurse elements: gotta get to the juicy text nodes inside the HTML
33 | gosh.wrapHangables(node);
34 | } else if(node.nodeType == 3) {
35 | gosh.wrapCharactersInTextNode(node);
36 | }
37 | }
38 |
39 | gosh.trimEmptyWrappers();
40 | }
41 |
42 | gosh.wrapCharactersInTextNode = function(node) {
43 | // For text nodes, wrap the hangable characters in spans and then wrap
44 | // that whole mess in another span so we can operate upon it in HTML
45 |
46 | var text = node.textContent,
47 | temp = document.createElement('span'),
48 | matchChars = new RegExp('[' + hangables.join('|') + ']', 'g');
49 |
50 | text = text.replace(matchChars, function(match) {
51 | return '' + match + '';
52 | });
53 |
54 | temp.setAttribute('data-hang-wrapper', 'true');
55 | temp.innerHTML = text;
56 |
57 | node.parentNode.insertBefore(temp, node);
58 | node.parentNode.removeChild(node);
59 | }
60 |
61 | gosh.trimEmptyWrappers = function() {
62 | var empties = document.querySelectorAll( '[data-hang-wrapper]' );
63 |
64 | for (var i = 0; i < empties.length; ++i) {
65 | var empty = empties[i];
66 |
67 | if (empty.innerHTML.match(/^\s*$/)) {
68 | empty.remove();
69 | }
70 | }
71 | }
72 |
73 | gosh.unwrapHangables = function(el) {
74 | // Unwrap hangable characters to get the DOM mostly back to normal
75 |
76 | var nodes = el.childNodes;
77 | console.log(nodes);
78 | }
79 |
80 | gosh.instantiateHangables = function() {
81 | // Make a new Hangable object to manage each hangable character
82 |
83 | var chars = document.querySelectorAll('[data-hang]');
84 |
85 | for (var i = 0; i < chars.length; ++i) {
86 | var char = chars[i];
87 |
88 | var hangable = new Hangable(char);
89 | }
90 | }
91 |
92 | gosh.doMatched = function(rules) {
93 | rules.each(function(rule) {
94 | var matchedEl = document.querySelectorAll( rule.getSelectors() )[0];
95 |
96 | if(matchedEl) {
97 | gosh.wrapHangables(matchedEl);
98 | gosh.instantiateHangables();
99 | }
100 | });
101 | }
102 |
103 | gosh.unhangAll = function(rules) {
104 | gosh.chars.forEach(function(char) {
105 | char.unhang().destroy();
106 | });
107 | }
108 |
109 | // Hangable object
110 | function Hangable(el) {
111 | this.el = el;
112 | this.container = this.getFirstBlockParent();
113 | this.wrapper = this.container.querySelector('[data-hang-wrapper]');
114 |
115 | gosh.chars.push(this);
116 |
117 | this.hang();
118 | }
119 |
120 | Hangable.prototype.getFirstBlockParent = function() {
121 | var el = this.el;
122 |
123 | // Loop through parent nodes until we find a block, or close enough
124 | while (el.parentNode) {
125 | el = el.parentNode;
126 | var display = getComputedStyle(el).display;
127 |
128 | if (display == "block" || display == "flex" || display == "grid") {
129 | return el;
130 | }
131 | }
132 | }
133 |
134 | Hangable.prototype.getRelativePosition = function() {
135 | var containerOffset, elOffset;
136 |
137 | containerOffset = this.getContainerTrueOffset();
138 | elOffset = this.el.offsetLeft;
139 |
140 | this.el.setAttribute('data-hang-position', elOffset - containerOffset);
141 | return elOffset - containerOffset;
142 | }
143 |
144 | Hangable.prototype.getRelativeWidth = function() {
145 | return (this.el.offsetWidth / this.getContainerWidth());
146 | }
147 |
148 | Hangable.prototype.hang = function() {
149 | this.el.style.marginLeft = '';
150 |
151 | if (this.getRelativePosition() == 0 && this.el === this.wrapper.firstElementChild) {
152 | this.el.style.marginLeft = (-100 * this.getRelativeWidth()) + '%';
153 |
154 | return true;
155 | }
156 |
157 | return false;
158 | }
159 |
160 | Hangable.prototype.unhang = function() {
161 | // get the element's parent node
162 | var parent = this.el.parentNode;
163 |
164 | // move all children out of the element
165 | while (this.el.firstChild) parent.insertBefore(this.el.firstChild, this.el);
166 |
167 | // remove the empty element
168 | parent.removeChild(this.el);
169 |
170 | return this;
171 | }
172 |
173 | Hangable.prototype.destroy = function() {
174 | delete this;
175 | }
176 |
177 | Hangable.prototype.getContainerTrueOffset = function() {
178 | // Get the container's offset, minus borders or any other interloping spaces
179 |
180 | var containerOffset = this.container.offsetLeft + this.container.clientLeft;
181 | return containerOffset;
182 | }
183 |
184 | Hangable.prototype.getContainerWidth = function() {
185 | return this.container.clientWidth;
186 | }
187 |
188 | // Set up the polyfill
189 | window.onload = function() {
190 | Polyfill({
191 | declarations: ["hanging-punctuation:first"]
192 | })
193 | .doMatched(gosh.doMatched)
194 | .undoUnmatched(gosh.unhangAll);
195 | }
196 |
197 | // Make it Responsive(tm)
198 | window.onresize = function() {
199 | gosh.chars.forEach(function(char) {
200 | char.hang();
201 | });
202 | }
203 |
204 | // Export this whole thing at the end
205 | window.goshHangIt = gosh;
206 |
207 | })(window, document);
208 |
--------------------------------------------------------------------------------
/lib/polyfill.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Polyfill.js - v0.1.0
3 | *
4 | * Copyright (c) 2015 Philip Walton
5 | * Released under the MIT license
6 | *
7 | * Date: 2015-06-21
8 | */
9 | ;(function(window, document, undefined){
10 |
11 | 'use strict';
12 |
13 | var reNative = RegExp('^' +
14 | String({}.valueOf)
15 | .replace(/[.*+?\^${}()|\[\]\\]/g, '\\$&')
16 | .replace(/valueOf|for [^\]]+/g, '.+?') + '$'
17 | )
18 |
19 |
20 | /**
21 | * Trim any leading or trailing whitespace
22 | */
23 | function trim(s) {
24 | return s.replace(/^\s+|\s+$/g,'')
25 | }
26 |
27 |
28 | /**
29 | * Detects the presence of an item in an array
30 | */
31 | function inArray(target, items) {
32 | var item
33 | , i = 0
34 | if (!target || !items) return false
35 | while(item = items[i++]) {
36 | if (target === item) return true
37 | }
38 | return false
39 | }
40 |
41 |
42 | /**
43 | * Determine if a method is support natively by the browser
44 | */
45 | function isNative(fn) {
46 | return reNative.test(fn)
47 | }
48 |
49 | /**
50 | * Determine if a URL is local to the document origin
51 | * Inspired form Respond.js
52 | * https://github.com/scottjehl/Respond/blob/master/respond.src.js#L90-L91
53 | */
54 | var isLocalURL = (function() {
55 | var base = document.getElementsByTagName("base")[0]
56 | , reProtocol = /^([a-zA-Z:]*\/\/)/
57 | return function(url) {
58 | var isLocal = (!reProtocol.test(url) && !base)
59 | || url.replace(RegExp.$1, "").split("/")[0] === location.host
60 | return isLocal
61 | }
62 | }())
63 |
64 | var supports = {
65 | // true with either native support or a polyfil, we don't care which
66 | matchMedia: window.matchMedia && window.matchMedia( "only all" ).matches,
67 | // true only if the browser supports window.matchMeida natively
68 | nativeMatchMedia: isNative(window.matchMedia)
69 | }
70 |
71 | var DownloadManager = (function() {
72 |
73 | var cache = {}
74 | , queue = []
75 | , callbacks = []
76 | , requestCount = 0
77 | , xhr = (function() {
78 | var method
79 | try { method = new window.XMLHttpRequest() }
80 | catch (e) { method = new window.ActiveXObject( "Microsoft.XMLHTTP" ) }
81 | return method
82 | }())
83 |
84 | // return function(urls, callback) {
85 |
86 | function addURLsToQueue(urls) {
87 | var url
88 | , i = 0
89 | while (url = urls[i++]) {
90 | if (!cache[url] && !inArray(url, queue)) {
91 | queue.push(url)
92 | }
93 | }
94 | }
95 |
96 | function processQueue() {
97 | // don't process the next one if we're in the middle of a download
98 | if (!(xhr.readyState === 0 || xhr.readyState === 4)) return
99 |
100 | var url
101 | if (url = queue[0]) {
102 | downloadStylesheet(url)
103 | }
104 | if (!url) {
105 | invokeCallbacks()
106 | }
107 | }
108 |
109 | /**
110 | * Make the requests
111 | *
112 | * TODO: Get simultaneous downloads working, it can't be that hard
113 | */
114 | function downloadStylesheet(url) {
115 | requestCount++
116 | xhr.open("GET", url, true)
117 | xhr.onreadystatechange = function () {
118 | if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
119 | cache[url] = xhr.responseText
120 | queue.shift()
121 | processQueue()
122 | }
123 | }
124 | xhr.send(null)
125 | }
126 |
127 | /**
128 | * Check the cache to make sure all requests are complete
129 | */
130 | function downloadsFinished(urls) {
131 | var url
132 | , i = 0
133 | , len = 0
134 | while (url = urls[i++]) {
135 | if (cache[url]) len++
136 | }
137 | return (len === urls.length)
138 | }
139 |
140 | /**
141 | * Invoke each callback and remove it from the list
142 | */
143 | function invokeCallbacks() {
144 | var callback
145 | while (callback = callbacks.shift()) {
146 | invokeCallback(callback.urls, callback.fn)
147 | }
148 | }
149 |
150 | /**
151 | * Put the stylesheets in the proper order and invoke the callback
152 | */
153 | function invokeCallback(urls, callback) {
154 | var stylesheets = []
155 | , url
156 | , i = 0
157 | while (url = urls[i++]) {
158 | stylesheets.push(cache[url])
159 | }
160 | callback.call(null, stylesheets)
161 | }
162 |
163 | return {
164 | request: function(urls, callback) {
165 | // Add the callback to the list
166 | callbacks.push({urls: urls, fn: callback})
167 |
168 | if (downloadsFinished(urls)) {
169 | invokeCallbacks()
170 | } else {
171 | addURLsToQueue(urls)
172 | processQueue()
173 | }
174 | },
175 | clearCache: function() {
176 | cache = {}
177 | },
178 | _getRequestCount: function() {
179 | return requestCount
180 | }
181 | }
182 |
183 | }())
184 |
185 | var StyleManager = {
186 |
187 | _cache: {},
188 |
189 | clearCache: function() {
190 | StyleManager._cache = {}
191 | },
192 |
193 | /**
194 | * Parse a string of CSS
195 | * optionaly pass an identifier for caching
196 | *
197 | * Adopted from TJ Holowaychuk's
198 | * https://github.com/visionmedia/css-parse
199 | *
200 | * Minor changes include removing the "stylesheet" root and
201 | * using String.charAt(i) instead of String[i] for IE7 compatibility
202 | */
203 | parse: function(css, identifier) {
204 |
205 | /**
206 | * Opening brace.
207 | */
208 | function open() {
209 | return match(/^\{\s*/)
210 | }
211 |
212 | /**
213 | * Closing brace.
214 | */
215 | function close() {
216 | return match(/^\}\s*/)
217 | }
218 |
219 | /**
220 | * Parse ruleset.
221 | */
222 | function rules() {
223 | var node
224 | var rules = []
225 | whitespace()
226 | comments(rules)
227 | while (css.charAt(0) != '}' && (node = atrule() || rule())) {
228 | rules.push(node)
229 | comments(rules)
230 | }
231 | return rules
232 | }
233 |
234 | /**
235 | * Match `re` and return captures.
236 | */
237 | function match(re) {
238 | var m = re.exec(css)
239 | if (!m) return
240 | css = css.slice(m[0].length)
241 | return m
242 | }
243 |
244 | /**
245 | * Parse whitespace.
246 | */
247 | function whitespace() {
248 | match(/^\s*/)
249 | }
250 |
251 | /**
252 | * Parse comments
253 | */
254 | function comments(rules) {
255 | rules = rules || []
256 | var c
257 | while (c = comment()) rules.push(c)
258 | return rules
259 | }
260 |
261 | /**
262 | * Parse comment.
263 | */
264 | function comment() {
265 | if ('/' == css[0] && '*' == css[1]) {
266 | var i = 2
267 | while ('*' != css[i] || '/' != css[i + 1]) ++i
268 | i += 2
269 | var comment = css.slice(2, i - 2)
270 | css = css.slice(i)
271 | whitespace()
272 | return { comment: comment }
273 | }
274 | }
275 |
276 | /**
277 | * Parse selector.
278 | */
279 | function selector() {
280 | var m = match(/^([^{]+)/)
281 | if (!m) return
282 | return trim(m[0]).split(/\s*,\s*/)
283 | }
284 |
285 | /**
286 | * Parse declaration.
287 | */
288 | function declaration() {
289 | // prop
290 | var prop = match(/^(\*?[\-\w]+)\s*/)
291 | if (!prop) return
292 | prop = prop[0]
293 |
294 | // :
295 | if (!match(/^:\s*/)) return
296 |
297 | // val
298 | var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)\s*/)
299 | if (!val) return
300 | val = trim(val[0])
301 |
302 | //
303 | match(/^[;\s]*/)
304 |
305 | return { property: prop, value: val }
306 | }
307 |
308 | /**
309 | * Parse keyframe.
310 | */
311 | function keyframe() {
312 | var m
313 | var vals = []
314 |
315 | while (m = match(/^(from|to|\d+%|\.\d+%|\d+\.\d+%)\s*/)) {
316 | vals.push(m[1])
317 | match(/^,\s*/)
318 | }
319 |
320 | if (!vals.length) return
321 |
322 | return {
323 | values: vals,
324 | declarations: declarations()
325 | }
326 | }
327 |
328 | /**
329 | * Parse keyframes.
330 | */
331 | function keyframes() {
332 | var m = match(/^@([\-\w]+)?keyframes */)
333 | if (!m) return
334 | var vendor = m[1]
335 |
336 | // identifier
337 | var m = match(/^([\-\w]+)\s*/)
338 | if (!m) return
339 | var name = m[1]
340 |
341 | if (!open()) return
342 | comments()
343 |
344 | var frame
345 | var frames = []
346 | while (frame = keyframe()) {
347 | frames.push(frame)
348 | comments()
349 | }
350 |
351 | if (!close()) return
352 |
353 | var obj = {
354 | name: name,
355 | keyframes: frames
356 | }
357 | // don't include vendor unles there's a match
358 | if (vendor) obj.vendor = vendor
359 |
360 | return obj
361 | }
362 |
363 | /**
364 | * Parse supports.
365 | */
366 | function supports() {
367 | var m = match(/^@supports *([^{]+)/)
368 | if (!m) return
369 | var supports = trim(m[1])
370 |
371 | if (!open()) return
372 | comments()
373 |
374 | var style = rules()
375 |
376 | if (!close()) return
377 |
378 | return { supports: supports, rules: style }
379 | }
380 |
381 | /**
382 | * Parse media.
383 | */
384 | function media() {
385 | var m = match(/^@media *([^{]+)/)
386 | if (!m) return
387 |
388 | var media = trim(m[1])
389 |
390 | if (!open()) return
391 | comments()
392 |
393 | var style = rules()
394 |
395 | if (!close()) return
396 |
397 | return { media: media, rules: style }
398 | }
399 |
400 |
401 | /**
402 | * Parse paged media.
403 | */
404 | function atpage() {
405 | var m = match(/^@page */)
406 | if (!m) return
407 |
408 | var sel = selector() || []
409 | var decls = []
410 |
411 | if (!open()) return
412 | comments()
413 |
414 | // declarations
415 | var decl
416 | while (decl = declaration() || atmargin()) {
417 | decls.push(decl)
418 | comments()
419 | }
420 |
421 | if (!close()) return
422 |
423 | return {
424 | type: "page",
425 | selectors: sel,
426 | declarations: decls
427 | }
428 | }
429 |
430 | /**
431 | * Parse margin at-rules
432 | */
433 | function atmargin() {
434 | var m = match(/^@([a-z\-]+) */)
435 | if (!m) return
436 | var type = m[1]
437 |
438 | return {
439 | type: type,
440 | declarations: declarations()
441 | }
442 | }
443 |
444 | /**
445 | * Parse import
446 | */
447 | function atimport() {
448 | return _atrule('import')
449 | }
450 |
451 | /**
452 | * Parse charset
453 | */
454 | function atcharset() {
455 | return _atrule('charset')
456 | }
457 |
458 | /**
459 | * Parse namespace
460 | */
461 | function atnamespace() {
462 | return _atrule('namespace')
463 | }
464 |
465 | /**
466 | * Parse non-block at-rules
467 | */
468 | function _atrule(name) {
469 | var m = match(new RegExp('^@' + name + ' *([^;\\n]+);\\s*'))
470 | if (!m) return
471 | var ret = {}
472 | ret[name] = trim(m[1])
473 | return ret
474 | }
475 |
476 | /**
477 | * Parse declarations.
478 | */
479 | function declarations() {
480 | var decls = []
481 |
482 | if (!open()) return
483 | comments()
484 |
485 | // declarations
486 | var decl
487 | while (decl = declaration()) {
488 | decls.push(decl)
489 | comments()
490 | }
491 |
492 | if (!close()) return
493 | return decls
494 | }
495 |
496 | /**
497 | * Parse at rule.
498 | */
499 | function atrule() {
500 | return keyframes()
501 | || media()
502 | || supports()
503 | || atimport()
504 | || atcharset()
505 | || atnamespace()
506 | || atpage()
507 | }
508 |
509 | /**
510 | * Parse rule.
511 | */
512 | function rule() {
513 | var sel = selector()
514 | if (!sel) return
515 | comments()
516 | return { selectors: sel, declarations: declarations() }
517 | }
518 |
519 | /**
520 | * Check the cache first, otherwise parse the CSS
521 | */
522 | if (identifier && StyleManager._cache[identifier]) {
523 | return StyleManager._cache[identifier]
524 | } else {
525 | // strip comments before parsing
526 | css = css.replace(/\/\*[\s\S]*?\*\//g, "")
527 | return StyleManager._cache[identifier] = rules()
528 | }
529 |
530 | },
531 |
532 | /**
533 | * Filter a ruleset by the passed keywords
534 | * Keywords may be either selector or property/value patterns
535 | */
536 | filter: function(rules, keywords) {
537 |
538 | var filteredRules = []
539 |
540 | /**
541 | * Concat a2 onto a1 even if a1 is undefined
542 | */
543 | function safeConcat(a1, a2) {
544 | if (!a1 && !a2) return
545 | if (!a1) return [a2]
546 | return a1.concat(a2)
547 | }
548 |
549 | /**
550 | * Add a rule to the filtered ruleset,
551 | * but don't add empty media or supports values
552 | */
553 | function addRule(rule) {
554 | if (rule.media == null) delete rule.media
555 | if (rule.supports == null) delete rule.supports
556 | filteredRules.push(rule)
557 | }
558 |
559 | function containsKeyword(string, keywordList) {
560 | if (!keywordList) return
561 | var i = keywordList.length
562 | while (i--) {
563 | if (string.indexOf(keywordList[i]) >= 0) return true
564 | }
565 | }
566 |
567 | function matchesKeywordPattern(declaration, patternList) {
568 | var wildcard = /\*/
569 | , pattern
570 | , parts
571 | , reProp
572 | , reValue
573 | , i = 0
574 | while (pattern = patternList[i++]) {
575 | parts = pattern.split(":")
576 | reProp = new RegExp("^" + trim(parts[0]).replace(wildcard, ".*") + "$")
577 | reValue = new RegExp("^" + trim(parts[1]).replace(wildcard, ".*") + "$")
578 | if (reProp.test(declaration.property) && reValue.test(declaration.value)) {
579 | return true
580 | }
581 | }
582 | }
583 |
584 | function matchSelectors(rule, media, supports) {
585 | if (!keywords.selectors) return
586 |
587 | if (containsKeyword(rule.selectors.join(","), keywords.selectors)) {
588 | addRule({
589 | media: media,
590 | supports: supports,
591 | selectors: rule.selectors,
592 | declarations: rule.declarations
593 | })
594 | return true
595 | }
596 | }
597 |
598 | function matchesDeclaration(rule, media, supports) {
599 | if (!keywords.declarations) return
600 | var declaration
601 | , i = 0
602 | while (declaration = rule.declarations[i++]) {
603 | if (matchesKeywordPattern(declaration, keywords.declarations)) {
604 | addRule({
605 | media: media,
606 | supports: supports,
607 | selectors: rule.selectors,
608 | declarations: rule.declarations
609 | })
610 | return true
611 | }
612 | }
613 | }
614 |
615 | function filterRules(rules, media, supports) {
616 | var rule
617 | , i = 0
618 | while (rule = rules[i++]) {
619 | if (rule.declarations) {
620 | matchSelectors(rule, media, supports) || matchesDeclaration(rule, media, supports)
621 | }
622 | else if (rule.rules && rule.media) {
623 | filterRules(rule.rules, safeConcat(media, rule.media), supports)
624 | }
625 | else if (rule.rules && rule.supports) {
626 | filterRules(rule.rules, media, safeConcat(supports, rule.supports))
627 | }
628 | }
629 |
630 | }
631 |
632 | // start the filtering
633 | filterRules(rules)
634 |
635 | // return the results
636 | return filteredRules
637 |
638 | }
639 | }
640 | var MediaManager = (function() {
641 |
642 | var reMinWidth = /\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/
643 | , reMaxWidth = /\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/
644 |
645 | // a cache of the active media query info
646 | , mediaQueryMap = {}
647 |
648 | // the value of an `em` as used in a media query,
649 | // not necessarily the base font-size
650 | , emValueInPixels
651 | , currentWidth
652 |
653 | /**
654 | * Get the pixel value of 1em for use in parsing media queries
655 | * ems in media queries are not affected by CSS, instead they
656 | * are the value of the browsers default font size, usually 16px
657 | */
658 | function getEmValueInPixels() {
659 |
660 | // cache this value because it probably won't change and
661 | // it's expensive to lookup
662 | if (emValueInPixels) return emValueInPixels
663 |
664 | var html = document.documentElement
665 | , body = document.body
666 | , originalHTMLFontSize = html.style.fontSize
667 | , originalBodyFontSize = body.style.fontSize
668 | , div = document.createElement("div")
669 |
670 | // 1em is the value of the default font size of the browser
671 | // reset html and body to ensure the correct value is returned
672 | html.style.fontSize = "1em"
673 | body.style.fontSize = "1em"
674 |
675 | // add a test element and measure it
676 | body.appendChild(div)
677 | div.style.width = "1em"
678 | div.style.position = "absolute"
679 | emValueInPixels = div.offsetWidth
680 |
681 | // remove the test element and restore the previous values
682 | body.removeChild(div)
683 | body.style.fontSize = originalBodyFontSize
684 | html.style.fontSize = originalHTMLFontSize
685 |
686 | return emValueInPixels
687 | }
688 |
689 | /**
690 | * Use the browsers matchMedia function or existing shim
691 | */
692 | function matchMediaNatively(query) {
693 | return window.matchMedia(query)
694 | }
695 |
696 | /**
697 | * Try to determine if a mediaQuery matches by
698 | * parsing the query and figuring it out manually
699 | * TODO: cache current width for repeated invocations
700 | */
701 | function matchMediaManually(query) {
702 | var minWidth
703 | , maxWidth
704 | , matches = false
705 |
706 | // recalculate the width if it's not set
707 | // if (!currentWidth) currentWidth = document.documentElement.clientWidth
708 | currentWidth = document.documentElement.clientWidth
709 |
710 | // parse min and max widths from query
711 | if (reMinWidth.test(query)) {
712 | minWidth = RegExp.$2 === "em"
713 | ? parseFloat(RegExp.$1) * getEmValueInPixels()
714 | : parseFloat(RegExp.$1)
715 | }
716 | if (reMaxWidth.test(query)) {
717 | maxWidth = RegExp.$2 === "em"
718 | ? parseFloat(RegExp.$1) * getEmValueInPixels()
719 | : parseFloat(RegExp.$1)
720 | }
721 |
722 | // if both minWith and maxWidth are set
723 | if (minWidth && maxWidth) {
724 | matches = (minWidth <= currentWidth && maxWidth >= currentWidth)
725 | } else {
726 | if (minWidth && minWidth <= currentWidth) matches = true
727 | if (maxWidth && maxWidth >= currentWidth) matches = true
728 | }
729 |
730 | // return fake MediaQueryList object
731 | return {
732 | matches: matches,
733 | media: query
734 | }
735 | }
736 |
737 |
738 | return {
739 | /**
740 | * Similar to the window.matchMedia method
741 | * results are cached to avoid expensive relookups
742 | * @returns MediaQueryList (or a faked one)
743 | */
744 | matchMedia: function(query) {
745 | return supports.matchMedia
746 | ? matchMediaNatively(query)
747 | : matchMediaManually(query)
748 | // return mediaQueryMap[query] || (
749 | // mediaQueryMap[query] = supports.matchMedia
750 | // ? matchMediaNatively(query)
751 | // : matchMediaManually(query)
752 | // )
753 | },
754 |
755 | clearCache: function() {
756 | // we don't use cache when the browser supports matchMedia listeners
757 | if (!supports.nativeMatchMedia) {
758 | currentWidth = null
759 | mediaQueryMap = {}
760 | }
761 | }
762 | }
763 |
764 | }())
765 |
766 | var EventManager = (function() {
767 |
768 | var MediaListener = (function() {
769 | var listeners = []
770 | return {
771 | add: function(polyfill, mql, fn) {
772 | var listener
773 | , i = 0
774 | // if the listener is already in the array, return false
775 | while (listener = listeners[i++]) {
776 | if (
777 | listener.polyfill == polyfill
778 | && listener.mql === mql
779 | && listener.fn === fn
780 | ) {
781 | return false
782 | }
783 | }
784 | // otherwise add it
785 | mql.addListener(fn)
786 | listeners.push({
787 | polyfill: polyfill,
788 | mql: mql,
789 | fn: fn
790 | })
791 | },
792 | remove: function(polyfill) {
793 | var listener
794 | , i = 0
795 | while (listener = listeners[i++]) {
796 | if (listener.polyfill === polyfill) {
797 | listener.mql.removeListener(listener.fn)
798 | listeners.splice(--i, 1)
799 | }
800 | }
801 | }
802 | }
803 | }())
804 |
805 | var ResizeListener = (function(listeners) {
806 | function onresize() {
807 | var listener
808 | , i = 0
809 | while (listener = listeners[i++]) {
810 | listener.fn()
811 | }
812 | }
813 | return {
814 | add: function(polyfill, fn) {
815 | if (!listeners.length) {
816 | if (window.addEventListener) {
817 | window.addEventListener("resize", onresize, false)
818 | } else {
819 | window.attachEvent("onresize", onresize)
820 | }
821 | }
822 | listeners.push({
823 | polyfill: polyfill,
824 | fn: fn
825 | })
826 |
827 | },
828 | remove: function(polyfill) {
829 | var listener
830 | , i = 0
831 | while (listener = listeners[i++]) {
832 | if (listener.polyfill === polyfill) {
833 | listeners.splice(--i, 1)
834 | }
835 | }
836 | if (!listeners.length) {
837 | if (window.removeEventListener) {
838 | window.removeEventListener("resize", onresize, false)
839 | } else if (window.detachEvent) {
840 | window.detachEvent("onresize", onresize)
841 | }
842 | }
843 | }
844 | }
845 | }([]))
846 |
847 |
848 | /**
849 | * Simple debounce function
850 | */
851 | function debounce(fn, wait) {
852 | var timeout
853 | return function() {
854 | clearTimeout(timeout)
855 | timeout = setTimeout(fn, wait)
856 | }
857 | }
858 |
859 | return {
860 |
861 | removeListeners: function(polyfill) {
862 | supports.nativeMatchMedia
863 | ? MediaListener.remove(polyfill)
864 | : ResizeListener.remove(polyfill)
865 | },
866 |
867 | addListeners: function(polyfill, callback) {
868 |
869 | var queries = polyfill._mediaQueryMap
870 | , state = {}
871 |
872 |
873 | /**
874 | * Set up initial state
875 | */
876 | ;(function() {
877 | for (var query in queries) {
878 | if (!queries.hasOwnProperty(query)) continue
879 | state[query] = MediaManager.matchMedia(query).matches
880 | }
881 | }())
882 |
883 | /**
884 | * Register the listeners to detect media query changes
885 | * if the browser doesn't support this natively, use resize events instead
886 | */
887 | function addListeners() {
888 |
889 | if (supports.nativeMatchMedia) {
890 | for (var query in queries) {
891 | if (queries.hasOwnProperty(query)) {
892 | // a closure is needed here to keep the variable reference
893 | (function(mql, query) {
894 | MediaListener.add(polyfill, mql, function() {
895 | callback.call(polyfill, query, mql.matches)
896 | })
897 | }(queries[query], query))
898 | }
899 | }
900 | } else {
901 |
902 | var fn = debounce((function(polyfill, queries) {
903 | return function() {
904 | updateMatchedMedia(polyfill, queries)
905 | }
906 | }(polyfill, queries)), polyfill._options.debounceTimeout || 100)
907 |
908 | ResizeListener.add(polyfill, fn)
909 |
910 | }
911 | }
912 |
913 | /**
914 | * Check each media query to see if it still matches
915 | * Note: this is only invoked when the browser doesn't
916 | * natively support window.matchMedia addListeners
917 | */
918 | function updateMatchedMedia(polyfill, queries) {
919 | var query
920 | , current = {}
921 |
922 | // clear the cache since a resize just happened
923 | MediaManager.clearCache()
924 |
925 | // look for media matches that have changed since the last inspection
926 | for (query in queries) {
927 | if (!queries.hasOwnProperty(query)) continue
928 | current[query] = MediaManager.matchMedia(query).matches
929 | if (current[query] != state[query]) {
930 | callback.call(polyfill, query, MediaManager.matchMedia(query).matches)
931 | }
932 | }
933 | state = current
934 | }
935 |
936 | addListeners()
937 |
938 | }
939 |
940 | }
941 |
942 | }())
943 |
944 | function Ruleset(rules) {
945 | var i = 0
946 | , rule
947 | this._rules = []
948 | while (rule = rules[i++]) {
949 | this._rules.push(new Rule(rule))
950 | }
951 | }
952 |
953 | Ruleset.prototype.each = function(iterator, context) {
954 | var rule
955 | , i = 0
956 | context || (context = this)
957 | while (rule = this._rules[i++]) {
958 | iterator.call(context, rule)
959 | }
960 | }
961 |
962 | Ruleset.prototype.size = function() {
963 | return this._rules.length
964 | }
965 |
966 | Ruleset.prototype.at = function(index) {
967 | return this._rules[index]
968 | }
969 |
970 | function Rule(rule) {
971 | this._rule = rule
972 | }
973 |
974 | Rule.prototype.getDeclaration = function() {
975 | var styles = {}
976 | , i = 0
977 | , declaration
978 | , declarations = this._rule.declarations
979 | while (declaration = declarations[i++]) {
980 | styles[declaration.property] = declaration.value
981 | }
982 | return styles
983 | }
984 |
985 | Rule.prototype.getSelectors = function() {
986 | return this._rule.selectors.join(", ")
987 | }
988 |
989 | Rule.prototype.getMedia = function() {
990 | return this._rule.media.join(" and ")
991 | }
992 |
993 | function Polyfill(options) {
994 |
995 | if (!(this instanceof Polyfill)) return new Polyfill(options)
996 |
997 | // set the options
998 | this._options = options
999 |
1000 | // allow the keywords option to be the only object passed
1001 | if (!options.keywords) this._options = { keywords: options }
1002 |
1003 | this._promise = []
1004 |
1005 | // then do the stuff
1006 | this._getStylesheets()
1007 | this._downloadStylesheets()
1008 | this._parseStylesheets()
1009 | this._filterCSSByKeywords()
1010 | this._buildMediaQueryMap()
1011 | this._reportInitialMatches()
1012 | this._addMediaListeners()
1013 | }
1014 |
1015 |
1016 | /**
1017 | * Fired when the media change and new rules match
1018 | */
1019 | Polyfill.prototype.doMatched = function(fn) {
1020 | this._doMatched = fn
1021 | this._resolve()
1022 | return this
1023 | }
1024 |
1025 |
1026 | /**
1027 | * Fired when the media changes and previously matching rules no longer match
1028 | */
1029 | Polyfill.prototype.undoUnmatched = function(fn) {
1030 | this._undoUnmatched = fn
1031 | this._resolve()
1032 | return this
1033 | }
1034 |
1035 |
1036 | /**
1037 | * Get all the rules the match the current media
1038 | */
1039 | Polyfill.prototype.getCurrentMatches = function() {
1040 | var i = 0
1041 | , rule
1042 | , media
1043 | , matches = []
1044 | while (rule = this._filteredRules[i++]) {
1045 | // rules are considered matches if they they have
1046 | // no media query or the media query curently matches
1047 | media = rule.media && rule.media.join(" and ")
1048 | if (!media || MediaManager.matchMedia(media).matches) {
1049 | matches.push(rule)
1050 | }
1051 | }
1052 | return new Ruleset(matches)
1053 | }
1054 |
1055 |
1056 | /**
1057 | * Destroy the instance
1058 | * Remove any bound events and send all current
1059 | * matches to the callback as unmatches
1060 | */
1061 | Polyfill.prototype.destroy = function() {
1062 | if (this._undoUnmatched) {
1063 | this._undoUnmatched(this.getCurrentMatches())
1064 | EventManager.removeListeners(this)
1065 | }
1066 | return
1067 | }
1068 |
1069 |
1070 | /**
1071 | * Defer a task until after a condition is met
1072 | */
1073 | Polyfill.prototype._defer = function(condition, callback) {
1074 | condition.call(this)
1075 | ? callback.call(this)
1076 | : this._promise.push({condition: condition, callback: callback})
1077 | }
1078 |
1079 |
1080 | /**
1081 | * Invoke any functions that have been deferred
1082 | */
1083 | Polyfill.prototype._resolve = function() {
1084 | var promise
1085 | , i = 0
1086 | while (promise = this._promise[i]) {
1087 | if (promise.condition.call(this)) {
1088 | this._promise.splice(i, 1)
1089 | promise.callback.call(this)
1090 | } else {
1091 | i++
1092 | }
1093 | }
1094 | }
1095 |
1096 |
1097 | /**
1098 | * Get a list of tags in the head
1099 | * optionally filter by the include/exclude options
1100 | */
1101 | Polyfill.prototype._getStylesheets = function() {
1102 | var i = 0
1103 | , id
1104 | , ids
1105 | , link
1106 | , links
1107 | , inline
1108 | , inlines
1109 | , stylesheet
1110 | , stylesheets = []
1111 |
1112 | if (this._options.include) {
1113 | // get only the included stylesheets link tags
1114 | ids = this._options.include
1115 | while (id = ids[i++]) {
1116 | if (link = document.getElementById(id)) {
1117 | // if this tag is an inline style
1118 | if (link.nodeName === "STYLE") {
1119 | stylesheet = { text: link.textContent }
1120 | stylesheets.push(stylesheet)
1121 | continue
1122 | }
1123 | // ignore print stylesheets
1124 | if (link.media && link.media == "print") continue
1125 | // ignore non-local stylesheets
1126 | if (!isLocalURL(link.href)) continue
1127 | stylesheet = { href: link.href }
1128 | link.media && (stylesheet.media = link.media)
1129 | stylesheets.push(stylesheet)
1130 | }
1131 | }
1132 | }
1133 | else {
1134 | // otherwise get all the stylesheets stylesheets tags
1135 | // except the explicitely exluded ones
1136 | ids = this._options.exclude
1137 | links = document.getElementsByTagName( "link" )
1138 | while (link = links[i++]) {
1139 | if (
1140 | link.rel
1141 | && (link.rel == "stylesheet")
1142 | && (link.media != "print") // ignore print stylesheets
1143 | && (isLocalURL(link.href)) // only request local stylesheets
1144 | && (!inArray(link.id, ids))
1145 | ) {
1146 | stylesheet = { href: link.href }
1147 | link.media && (stylesheet.media = link.media)
1148 | stylesheets.push(stylesheet)
1149 | }
1150 | }
1151 | inlines = document.getElementsByTagName('style');
1152 | i = 0;
1153 | while (inline = inlines[i++]){
1154 | stylesheet = { text: inline.textContent }
1155 | stylesheets.push(stylesheet);
1156 | }
1157 | }
1158 | return this._stylesheets = stylesheets
1159 | }
1160 |
1161 |
1162 | /**
1163 | * Download each stylesheet in the _stylesheetURLs array
1164 | */
1165 | Polyfill.prototype._downloadStylesheets = function() {
1166 | var self = this
1167 | , stylesheet
1168 | , urls = []
1169 | , i = 0
1170 | while (stylesheet = this._stylesheets[i++]) {
1171 | urls.push(stylesheet.href)
1172 | }
1173 | DownloadManager.request(urls, function(stylesheets) {
1174 | var stylesheet
1175 | , i = 0
1176 | while (stylesheet = stylesheets[i]) {
1177 | self._stylesheets[i++].text = stylesheet
1178 | }
1179 | self._resolve()
1180 | })
1181 | }
1182 |
1183 | Polyfill.prototype._parseStylesheets = function() {
1184 | this._defer(
1185 | function() {
1186 | return this._stylesheets
1187 | && this._stylesheets.length
1188 | && this._stylesheets[0].text },
1189 | function() {
1190 | var i = 0
1191 | , stylesheet
1192 | while (stylesheet = this._stylesheets[i++]) {
1193 | stylesheet.rules = StyleManager.parse(stylesheet.text, stylesheet.url)
1194 | }
1195 | }
1196 | )
1197 | }
1198 |
1199 | Polyfill.prototype._filterCSSByKeywords = function() {
1200 | this._defer(
1201 | function() {
1202 | return this._stylesheets
1203 | && this._stylesheets.length
1204 | && this._stylesheets[0].rules
1205 | },
1206 | function() {
1207 | var stylesheet
1208 | , media
1209 | , rules = []
1210 | , i = 0
1211 | while (stylesheet = this._stylesheets[i++]) {
1212 | media = stylesheet.media
1213 | // Treat stylesheets with a media attribute as being contained inside
1214 | // a single @media block, but ignore `all` and `screen` media values
1215 | // since they're basically meaningless in this context
1216 | if (media && media != "all" && media != "screen") {
1217 | rules.push({rules: stylesheet.rules, media: stylesheet.media})
1218 | } else {
1219 | rules = rules.concat(stylesheet.rules)
1220 | }
1221 | }
1222 | this._filteredRules = StyleManager.filter(rules, this._options.keywords)
1223 | }
1224 | )
1225 | }
1226 |
1227 | Polyfill.prototype._buildMediaQueryMap = function() {
1228 | this._defer(
1229 | function() { return this._filteredRules },
1230 | function() {
1231 | var i = 0
1232 | , media
1233 | , rule
1234 | this._mediaQueryMap = {}
1235 | while (rule = this._filteredRules[i++]) {
1236 | if (rule.media) {
1237 | media = rule.media.join(" and ")
1238 | this._mediaQueryMap[media] = MediaManager.matchMedia(media)
1239 | }
1240 | }
1241 | }
1242 | )
1243 | }
1244 |
1245 | Polyfill.prototype._reportInitialMatches = function() {
1246 | this._defer(
1247 | function() {
1248 | return this._filteredRules && this._doMatched
1249 | },
1250 | function() {
1251 | this._doMatched(this.getCurrentMatches())
1252 | }
1253 | )
1254 | }
1255 |
1256 | Polyfill.prototype._addMediaListeners = function() {
1257 | this._defer(
1258 | function() {
1259 | return this._filteredRules
1260 | && this._doMatched
1261 | && this._undoUnmatched
1262 | },
1263 | function() {
1264 | EventManager.addListeners(
1265 | this,
1266 | function(query, isMatch) {
1267 | var i = 0
1268 | , rule
1269 | , matches = []
1270 | , unmatches = []
1271 | while (rule = this._filteredRules[i++]) {
1272 | if (rule.media && rule.media.join(" and ") == query) {
1273 | (isMatch ? matches : unmatches).push(rule)
1274 | }
1275 | }
1276 | matches.length && this._doMatched(new Ruleset(matches))
1277 | unmatches.length && this._undoUnmatched(new Ruleset(unmatches))
1278 | }
1279 | )
1280 | }
1281 | )
1282 | }
1283 |
1284 | Polyfill.modules = {
1285 | DownloadManager: DownloadManager,
1286 | StyleManager: StyleManager,
1287 | MediaManager: MediaManager,
1288 | EventManager: EventManager
1289 | }
1290 | Polyfill.constructors = {
1291 | Ruleset: Ruleset,
1292 | Rule: Rule
1293 | }
1294 |
1295 | window.Polyfill = Polyfill
1296 |
1297 | }(window, document));
--------------------------------------------------------------------------------