Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
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)": {...}}`.