148 lines
3.4 KiB
JavaScript
148 lines
3.4 KiB
JavaScript
/**
|
||
* @typedef {import('hast').Element} Element
|
||
* @typedef {import('hast').Parents} Parents
|
||
*/
|
||
|
||
import {whitespace} from 'hast-util-whitespace'
|
||
import {siblingAfter, siblingBefore} from './util/siblings.js'
|
||
import {closing} from './closing.js'
|
||
import {omission} from './omission.js'
|
||
|
||
export const opening = omission({
|
||
body,
|
||
colgroup,
|
||
head,
|
||
html,
|
||
tbody
|
||
})
|
||
|
||
/**
|
||
* Whether to omit `<html>`.
|
||
*
|
||
* @param {Element} node
|
||
* Element.
|
||
* @returns {boolean}
|
||
* Whether the opening tag can be omitted.
|
||
*/
|
||
function html(node) {
|
||
const head = siblingAfter(node, -1)
|
||
return !head || head.type !== 'comment'
|
||
}
|
||
|
||
/**
|
||
* Whether to omit `<head>`.
|
||
*
|
||
* @param {Element} node
|
||
* Element.
|
||
* @returns {boolean}
|
||
* Whether the opening tag can be omitted.
|
||
*/
|
||
function head(node) {
|
||
const children = node.children
|
||
/** @type {Array<string>} */
|
||
const seen = []
|
||
let index = -1
|
||
|
||
while (++index < children.length) {
|
||
const child = children[index]
|
||
if (
|
||
child.type === 'element' &&
|
||
(child.tagName === 'title' || child.tagName === 'base')
|
||
) {
|
||
if (seen.includes(child.tagName)) return false
|
||
seen.push(child.tagName)
|
||
}
|
||
}
|
||
|
||
return children.length > 0
|
||
}
|
||
|
||
/**
|
||
* Whether to omit `<body>`.
|
||
*
|
||
* @param {Element} node
|
||
* Element.
|
||
* @returns {boolean}
|
||
* Whether the opening tag can be omitted.
|
||
*/
|
||
function body(node) {
|
||
const head = siblingAfter(node, -1, true)
|
||
|
||
return (
|
||
!head ||
|
||
(head.type !== 'comment' &&
|
||
!(head.type === 'text' && whitespace(head.value.charAt(0))) &&
|
||
!(
|
||
head.type === 'element' &&
|
||
(head.tagName === 'meta' ||
|
||
head.tagName === 'link' ||
|
||
head.tagName === 'script' ||
|
||
head.tagName === 'style' ||
|
||
head.tagName === 'template')
|
||
))
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Whether to omit `<colgroup>`.
|
||
* The spec describes some logic for the opening tag, but it’s easier to
|
||
* implement in the closing tag, to the same effect, so we handle it there
|
||
* instead.
|
||
*
|
||
* @param {Element} node
|
||
* Element.
|
||
* @param {number | undefined} index
|
||
* Index of element in parent.
|
||
* @param {Parents | undefined} parent
|
||
* Parent of element.
|
||
* @returns {boolean}
|
||
* Whether the opening tag can be omitted.
|
||
*/
|
||
function colgroup(node, index, parent) {
|
||
const previous = siblingBefore(parent, index)
|
||
const head = siblingAfter(node, -1, true)
|
||
|
||
// Previous colgroup was already omitted.
|
||
if (
|
||
parent &&
|
||
previous &&
|
||
previous.type === 'element' &&
|
||
previous.tagName === 'colgroup' &&
|
||
closing(previous, parent.children.indexOf(previous), parent)
|
||
) {
|
||
return false
|
||
}
|
||
|
||
return Boolean(head && head.type === 'element' && head.tagName === 'col')
|
||
}
|
||
|
||
/**
|
||
* Whether to omit `<tbody>`.
|
||
*
|
||
* @param {Element} node
|
||
* Element.
|
||
* @param {number | undefined} index
|
||
* Index of element in parent.
|
||
* @param {Parents | undefined} parent
|
||
* Parent of element.
|
||
* @returns {boolean}
|
||
* Whether the opening tag can be omitted.
|
||
*/
|
||
function tbody(node, index, parent) {
|
||
const previous = siblingBefore(parent, index)
|
||
const head = siblingAfter(node, -1)
|
||
|
||
// Previous table section was already omitted.
|
||
if (
|
||
parent &&
|
||
previous &&
|
||
previous.type === 'element' &&
|
||
(previous.tagName === 'thead' || previous.tagName === 'tbody') &&
|
||
closing(previous, parent.children.indexOf(previous), parent)
|
||
) {
|
||
return false
|
||
}
|
||
|
||
return Boolean(head && head.type === 'element' && head.tagName === 'tr')
|
||
}
|