forked from rehypejs/rehype-minify
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
138 lines (120 loc) · 3.05 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* rehype plugin to sort attributes.
*
* ## What is this?
*
* This package is a plugin that sorts attributes based on how frequent they
* occur, which optimizes for repetition-based compression (such as GZip).
*
* ## When should I use this?
*
* You can use this plugin when you want to improve the transfer size of HTML
* documents.
*
* ## API
*
* ### `unified().use(rehypeSortAttributeValues)`
*
* Sort attributes.
* There are no options.
*
* @example
* <div id="foo">bar</div>
* <div class="baz" id="qux">quux</div>
*/
/**
* @typedef {import('hast').Root} Root
* @typedef {import('hast').Element} Element
*/
import {visit} from 'unist-util-visit'
const own = {}.hasOwnProperty
/**
* Sort attributes.
*
* @type {import('unified').Plugin<Array<void>, Root>}
*/
export default function rehypeSortAttributes() {
return (tree) => {
/** @type {Record<string, {known: Array<string>, counts: Record<string, number>}>} */
const counts = {}
visit(tree, 'element', (node) => {
const name = node.tagName
const cache = counts[name] || (counts[name] = {known: [], counts: {}})
const props = node.properties || {}
/** @type {string} */
let prop
for (prop in props) {
if (own.call(props, prop)) {
const value = safe(prop)
if (value in cache.counts) {
cache.counts[value]++
} else {
cache.counts[value] = 1
cache.known.push(prop)
}
}
}
})
const caches = optimize()
visit(tree, 'element', (node) => {
const cache = caches[node.tagName]
const props = node.properties || {}
/** @type {Array<string>} */
const keys = []
/** @type {Required<Element['properties']>} */
const result = {}
let index = -1
/** @type {string} */
let prop
for (prop in props) {
if (own.call(props, prop)) {
keys.push(prop)
}
}
keys.sort((a, b) => cache.indexOf(a) - cache.indexOf(b))
while (++index < keys.length) {
result[keys[index]] = props[keys[index]]
}
node.properties = result
})
function optimize() {
/** @type {Record<string, Array<string>>} */
const caches = {}
/** @type {string} */
let name
for (name in counts) {
if (own.call(counts, name)) {
const values = counts[name]
caches[name] = values.known.sort(
(a, b) =>
values.counts[safe(b)] - values.counts[safe(a)] ||
compare(a, b, 0)
)
}
}
return caches
}
}
}
/**
* @param {string} value
* @returns {string}
*/
function safe(value) {
return '$' + value
}
/**
* This would create an infinite loop if `a` and `b` could be equal, but the
* list we operate on only has unique values.
*
* @param {string} a
* @param {string} b
* @param {number} index
* @returns {number}
*/
function compare(a, b, index) {
return (
(a.charCodeAt(index) || 0) - (b.charCodeAt(index) || 0) ||
compare(a, b, index + 1)
)
}