Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com
at 44f0460002641e463edef28889ad4e76b4df75b6 172 lines 6.9 kB view raw
1const C = "\u037c" 2const COUNT = typeof Symbol == "undefined" ? "__" + C : Symbol.for(C) 3const SET = typeof Symbol == "undefined" ? "__styleSet" + Math.floor(Math.random() * 1e8) : Symbol("styleSet") 4const top = typeof globalThis != "undefined" ? globalThis : typeof window != "undefined" ? window : {} 5 6// :: - Style modules encapsulate a set of CSS rules defined from 7// JavaScript. Their definitions are only available in a given DOM 8// root after it has been _mounted_ there with `StyleModule.mount`. 9// 10// Style modules should be created once and stored somewhere, as 11// opposed to re-creating them every time you need them. The amount of 12// CSS rules generated for a given DOM root is bounded by the amount 13// of style modules that were used. So to avoid leaking rules, don't 14// create these dynamically, but treat them as one-time allocations. 15export class StyleModule { 16 // :: (Object<Style>, ?{finish: ?(string) → string}) 17 // Create a style module from the given spec. 18 // 19 // When `finish` is given, it is called on regular (non-`@`) 20 // selectors (after `&` expansion) to compute the final selector. 21 constructor(spec, options) { 22 this.rules = [] 23 let {finish} = options || {} 24 25 function splitSelector(selector) { 26 return /^@/.test(selector) ? [selector] : selector.split(/,\s*/) 27 } 28 29 function render(selectors, spec, target, isKeyframes) { 30 let local = [], isAt = /^@(\w+)\b/.exec(selectors[0]), keyframes = isAt && isAt[1] == "keyframes" 31 if (isAt && spec == null) return target.push(selectors[0] + ";") 32 for (let prop in spec) { 33 let value = spec[prop] 34 if (/&/.test(prop)) { 35 render(prop.split(/,\s*/).map(part => selectors.map(sel => part.replace(/&/, sel))).reduce((a, b) => a.concat(b)), 36 value, target) 37 } else if (value && typeof value == "object") { 38 if (!isAt) throw new RangeError("The value of a property (" + prop + ") should be a primitive value.") 39 render(splitSelector(prop), value, local, keyframes) 40 } else if (value != null) { 41 local.push(prop.replace(/_.*/, "").replace(/[A-Z]/g, l => "-" + l.toLowerCase()) + ": " + value + ";") 42 } 43 } 44 if (local.length || keyframes) { 45 target.push((finish && !isAt && !isKeyframes ? selectors.map(finish) : selectors).join(", ") + 46 " {" + local.join(" ") + "}") 47 } 48 } 49 50 for (let prop in spec) render(splitSelector(prop), spec[prop], this.rules) 51 } 52 53 // :: () → string 54 // Returns a string containing the module's CSS rules. 55 getRules() { return this.rules.join("\n") } 56 57 // :: () → string 58 // Generate a new unique CSS class name. 59 static newName() { 60 let id = top[COUNT] || 1 61 top[COUNT] = id + 1 62 return C + id.toString(36) 63 } 64 65 // :: (union<Document, ShadowRoot>, union<[StyleModule], StyleModule>, ?{nonce: ?string}) 66 // 67 // Mount the given set of modules in the given DOM root, which ensures 68 // that the CSS rules defined by the module are available in that 69 // context. 70 // 71 // Rules are only added to the document once per root. 72 // 73 // Rule order will follow the order of the modules, so that rules from 74 // modules later in the array take precedence of those from earlier 75 // modules. If you call this function multiple times for the same root 76 // in a way that changes the order of already mounted modules, the old 77 // order will be changed. 78 // 79 // If a Content Security Policy nonce is provided, it is added to 80 // the `<style>` tag generated by the library. 81 static mount(root, modules, options) { 82 let set = root[SET], nonce = options && options.nonce 83 if (!set) set = new StyleSet(root, nonce) 84 else if (nonce) set.setNonce(nonce) 85 set.mount(Array.isArray(modules) ? modules : [modules], root) 86 } 87} 88 89let adoptedSet = new Map //<Document, StyleSet> 90 91class StyleSet { 92 constructor(root, nonce) { 93 let doc = root.ownerDocument || root, win = doc.defaultView 94 if (!root.head && root.adoptedStyleSheets && win.CSSStyleSheet) { 95 let adopted = adoptedSet.get(doc) 96 if (adopted) return root[SET] = adopted 97 this.sheet = new win.CSSStyleSheet 98 adoptedSet.set(doc, this) 99 } else { 100 this.styleTag = doc.createElement("style") 101 if (nonce) this.styleTag.setAttribute("nonce", nonce) 102 } 103 this.modules = [] 104 root[SET] = this 105 } 106 107 mount(modules, root) { 108 let sheet = this.sheet 109 let pos = 0 /* Current rule offset */, j = 0 /* Index into this.modules */ 110 for (let i = 0; i < modules.length; i++) { 111 let mod = modules[i], index = this.modules.indexOf(mod) 112 if (index < j && index > -1) { // Ordering conflict 113 this.modules.splice(index, 1) 114 j-- 115 index = -1 116 } 117 if (index == -1) { 118 this.modules.splice(j++, 0, mod) 119 if (sheet) for (let k = 0; k < mod.rules.length; k++) 120 sheet.insertRule(mod.rules[k], pos++) 121 } else { 122 while (j < index) pos += this.modules[j++].rules.length 123 pos += mod.rules.length 124 j++ 125 } 126 } 127 128 if (sheet) { 129 if (root.adoptedStyleSheets.indexOf(this.sheet) < 0) 130 root.adoptedStyleSheets = [this.sheet, ...root.adoptedStyleSheets] 131 } else { 132 let text = "" 133 for (let i = 0; i < this.modules.length; i++) 134 text += this.modules[i].getRules() + "\n" 135 this.styleTag.textContent = text 136 let target = root.head || root 137 if (this.styleTag.parentNode != target) 138 target.insertBefore(this.styleTag, target.firstChild) 139 } 140 } 141 142 setNonce(nonce) { 143 if (this.styleTag && this.styleTag.getAttribute("nonce") != nonce) 144 this.styleTag.setAttribute("nonce", nonce) 145 } 146} 147 148// Style::Object<union<Style,string>> 149// 150// A style is an object that, in the simple case, maps CSS property 151// names to strings holding their values, as in `{color: "red", 152// fontWeight: "bold"}`. The property names can be given in 153// camel-case—the library will insert a dash before capital letters 154// when converting them to CSS. 155// 156// If you include an underscore in a property name, it and everything 157// after it will be removed from the output, which can be useful when 158// providing a property multiple times, for browser compatibility 159// reasons. 160// 161// A property in a style object can also be a sub-selector, which 162// extends the current context to add a pseudo-selector or a child 163// selector. Such a property should contain a `&` character, which 164// will be replaced by the current selector. For example `{"&:before": 165// {content: '"hi"'}}`. Sub-selectors and regular properties can 166// freely be mixed in a given object. Any property containing a `&` is 167// assumed to be a sub-selector. 168// 169// Finally, a property can specify an @-block to be wrapped around the 170// styles defined inside the object that's the property's value. For 171// example to create a media query you can do `{"@media screen and 172// (min-width: 400px)": {...}}`.