131 lines
2.9 KiB
JavaScript
131 lines
2.9 KiB
JavaScript
|
|
/**
|
|||
|
|
* @import {Root} from 'hast'
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import {isElement} from 'hast-util-is-element'
|
|||
|
|
import {visit} from 'unist-util-visit'
|
|||
|
|
import {schema} from './schema.js'
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Sort attribute values.
|
|||
|
|
*
|
|||
|
|
* @returns
|
|||
|
|
* Transform.
|
|||
|
|
*/
|
|||
|
|
export default function rehypeSortAttributeValues() {
|
|||
|
|
/**
|
|||
|
|
* @param {Root} tree
|
|||
|
|
* Tree.
|
|||
|
|
* @returns {undefined}
|
|||
|
|
* Nothing.
|
|||
|
|
*/
|
|||
|
|
return function (tree) {
|
|||
|
|
// Map of properties to property values to counts.
|
|||
|
|
/** @type {Map<string, Map<number | string, number>>} */
|
|||
|
|
const counts = new Map()
|
|||
|
|
// List of all arrays, with their property names, so we don’t walk twice.
|
|||
|
|
/** @type {Array<[string, Array<number | string>]>} */
|
|||
|
|
const queues = []
|
|||
|
|
|
|||
|
|
visit(tree, 'element', function (node) {
|
|||
|
|
/** @type {string} */
|
|||
|
|
let property
|
|||
|
|
|
|||
|
|
for (property in node.properties) {
|
|||
|
|
if (Object.hasOwn(node.properties, property)) {
|
|||
|
|
const value = node.properties[property]
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
Object.hasOwn(schema, property) &&
|
|||
|
|
isElement(node, schema[property]) &&
|
|||
|
|
Array.isArray(value)
|
|||
|
|
) {
|
|||
|
|
add(property, value)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
flush()
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @param {string} property
|
|||
|
|
* Property name.
|
|||
|
|
* @param {Array<number | string>} values
|
|||
|
|
* Values.
|
|||
|
|
* @returns {undefined}
|
|||
|
|
* Nothing.
|
|||
|
|
*/
|
|||
|
|
function add(property, values) {
|
|||
|
|
let index = -1
|
|||
|
|
let cache = counts.get(property)
|
|||
|
|
|
|||
|
|
if (!cache) {
|
|||
|
|
cache = new Map()
|
|||
|
|
counts.set(property, cache)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
while (++index < values.length) {
|
|||
|
|
const value = values[index]
|
|||
|
|
cache.set(value, (cache.get(value) || 0) + 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
queues.push([property, values])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @returns {undefined}
|
|||
|
|
* Nothing.
|
|||
|
|
*/
|
|||
|
|
function flush() {
|
|||
|
|
/** @type {Map<string, Array<number | string>>} */
|
|||
|
|
const caches = new Map()
|
|||
|
|
|
|||
|
|
for (const [property, cache] of counts) {
|
|||
|
|
caches.set(
|
|||
|
|
property,
|
|||
|
|
[...cache.entries()]
|
|||
|
|
.sort(function (a, b) {
|
|||
|
|
return b[1] - a[1] || compare(String(a[0]), String(b[0]), 0)
|
|||
|
|
})
|
|||
|
|
.map(function (d) {
|
|||
|
|
return d[0]
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let index = -1
|
|||
|
|
|
|||
|
|
while (++index < queues.length) {
|
|||
|
|
const queue = queues[index]
|
|||
|
|
const cache = caches.get(queue[0])
|
|||
|
|
if (cache) {
|
|||
|
|
queue[1].sort(function (a, b) {
|
|||
|
|
return cache.indexOf(a) - cache.indexOf(b)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 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
|
|||
|
|
* Left value.
|
|||
|
|
* @param {string} b
|
|||
|
|
* Right value.
|
|||
|
|
* @param {number} index
|
|||
|
|
* Current index in values.
|
|||
|
|
* @returns {number}
|
|||
|
|
* Order.
|
|||
|
|
*/
|
|||
|
|
function compare(a, b, index) {
|
|||
|
|
return (
|
|||
|
|
(a.charCodeAt(index) || 0) - (b.charCodeAt(index) || 0) ||
|
|||
|
|
compare(a, b, index + 1)
|
|||
|
|
)
|
|||
|
|
}
|