New source found from dndbeyond.com

This commit is contained in:
2026-07-08 01:00:09 -07:00
parent 9a983a6d7b
commit dcefa0d2f2
2865 changed files with 222467 additions and 49053 deletions
+787
View File
@@ -0,0 +1,787 @@
/**
* @import {Identifier, Literal, MemberExpression} from 'estree'
* @import {Jsx, JsxDev, Options, Props} from 'hast-util-to-jsx-runtime'
* @import {Element, Nodes, Parents, Root, Text} from 'hast'
* @import {MdxFlowExpressionHast, MdxTextExpressionHast} from 'mdast-util-mdx-expression'
* @import {MdxJsxFlowElementHast, MdxJsxTextElementHast} from 'mdast-util-mdx-jsx'
* @import {MdxjsEsmHast} from 'mdast-util-mdxjs-esm'
* @import {Position} from 'unist'
* @import {Child, Create, Field, JsxElement, State, Style} from './types.js'
*/
import {stringify as commas} from 'comma-separated-tokens'
import {ok as assert} from 'devlop'
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
import {whitespace} from 'hast-util-whitespace'
import {find, hastToReact, html, svg} from 'property-information'
import {stringify as spaces} from 'space-separated-tokens'
import styleToJs from 'style-to-js'
import {pointStart} from 'unist-util-position'
import {VFileMessage} from 'vfile-message'
// To do: next major: `Object.hasOwn`.
const own = {}.hasOwnProperty
/** @type {Map<string, number>} */
const emptyMap = new Map()
const cap = /[A-Z]/g
// `react-dom` triggers a warning for *any* white space in tables.
// To follow GFM, `mdast-util-to-hast` injects line endings between elements.
// Other tools might do so too, but they dont do here, so we remove all of
// that.
// See: <https://github.com/facebook/react/pull/7081>.
// See: <https://github.com/facebook/react/pull/7515>.
// See: <https://github.com/remarkjs/remark-react/issues/64>.
// See: <https://github.com/rehypejs/rehype-react/pull/29>.
// See: <https://github.com/rehypejs/rehype-react/pull/32>.
// See: <https://github.com/rehypejs/rehype-react/pull/45>.
const tableElements = new Set(['table', 'tbody', 'thead', 'tfoot', 'tr'])
const tableCellElement = new Set(['td', 'th'])
const docs = 'https://github.com/syntax-tree/hast-util-to-jsx-runtime'
/**
* Transform a hast tree to preact, react, solid, svelte, vue, etc.,
* with an automatic JSX runtime.
*
* @param {Nodes} tree
* Tree to transform.
* @param {Options} options
* Configuration (required).
* @returns {JsxElement}
* JSX element.
*/
export function toJsxRuntime(tree, options) {
if (!options || options.Fragment === undefined) {
throw new TypeError('Expected `Fragment` in options')
}
const filePath = options.filePath || undefined
/** @type {Create} */
let create
if (options.development) {
if (typeof options.jsxDEV !== 'function') {
throw new TypeError(
'Expected `jsxDEV` in options when `development: true`'
)
}
create = developmentCreate(filePath, options.jsxDEV)
} else {
if (typeof options.jsx !== 'function') {
throw new TypeError('Expected `jsx` in production options')
}
if (typeof options.jsxs !== 'function') {
throw new TypeError('Expected `jsxs` in production options')
}
create = productionCreate(filePath, options.jsx, options.jsxs)
}
/** @type {State} */
const state = {
Fragment: options.Fragment,
ancestors: [],
components: options.components || {},
create,
elementAttributeNameCase: options.elementAttributeNameCase || 'react',
evaluater: options.createEvaluater ? options.createEvaluater() : undefined,
filePath,
ignoreInvalidStyle: options.ignoreInvalidStyle || false,
passKeys: options.passKeys !== false,
passNode: options.passNode || false,
schema: options.space === 'svg' ? svg : html,
stylePropertyNameCase: options.stylePropertyNameCase || 'dom',
tableCellAlignToStyle: options.tableCellAlignToStyle !== false
}
const result = one(state, tree, undefined)
// JSX element.
if (result && typeof result !== 'string') {
return result
}
// Text node or something that turned into nothing.
return state.create(
tree,
state.Fragment,
{children: result || undefined},
undefined
)
}
/**
* Transform a node.
*
* @param {State} state
* Info passed around.
* @param {Nodes} node
* Current node.
* @param {string | undefined} key
* Key.
* @returns {Child | undefined}
* Child, optional.
*/
function one(state, node, key) {
if (node.type === 'element') {
return element(state, node, key)
}
if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') {
return mdxExpression(state, node)
}
if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
return mdxJsxElement(state, node, key)
}
if (node.type === 'mdxjsEsm') {
return mdxEsm(state, node)
}
if (node.type === 'root') {
return root(state, node, key)
}
if (node.type === 'text') {
return text(state, node)
}
}
/**
* Handle element.
*
* @param {State} state
* Info passed around.
* @param {Element} node
* Current node.
* @param {string | undefined} key
* Key.
* @returns {Child | undefined}
* Child, optional.
*/
function element(state, node, key) {
const parentSchema = state.schema
let schema = parentSchema
if (node.tagName.toLowerCase() === 'svg' && parentSchema.space === 'html') {
schema = svg
state.schema = schema
}
state.ancestors.push(node)
const type = findComponentFromName(state, node.tagName, false)
const props = createElementProps(state, node)
let children = createChildren(state, node)
if (tableElements.has(node.tagName)) {
children = children.filter(function (child) {
return typeof child === 'string' ? !whitespace(child) : true
})
}
addNode(state, props, type, node)
addChildren(props, children)
// Restore.
state.ancestors.pop()
state.schema = parentSchema
return state.create(node, type, props, key)
}
/**
* Handle MDX expression.
*
* @param {State} state
* Info passed around.
* @param {MdxFlowExpressionHast | MdxTextExpressionHast} node
* Current node.
* @returns {Child | undefined}
* Child, optional.
*/
function mdxExpression(state, node) {
if (node.data && node.data.estree && state.evaluater) {
const program = node.data.estree
const expression = program.body[0]
assert(expression.type === 'ExpressionStatement')
// Assume result is a child.
return /** @type {Child | undefined} */ (
state.evaluater.evaluateExpression(expression.expression)
)
}
crashEstree(state, node.position)
}
/**
* Handle MDX ESM.
*
* @param {State} state
* Info passed around.
* @param {MdxjsEsmHast} node
* Current node.
* @returns {Child | undefined}
* Child, optional.
*/
function mdxEsm(state, node) {
if (node.data && node.data.estree && state.evaluater) {
// Assume result is a child.
return /** @type {Child | undefined} */ (
state.evaluater.evaluateProgram(node.data.estree)
)
}
crashEstree(state, node.position)
}
/**
* Handle MDX JSX.
*
* @param {State} state
* Info passed around.
* @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node
* Current node.
* @param {string | undefined} key
* Key.
* @returns {Child | undefined}
* Child, optional.
*/
function mdxJsxElement(state, node, key) {
const parentSchema = state.schema
let schema = parentSchema
if (node.name === 'svg' && parentSchema.space === 'html') {
schema = svg
state.schema = schema
}
state.ancestors.push(node)
const type =
node.name === null
? state.Fragment
: findComponentFromName(state, node.name, true)
const props = createJsxElementProps(state, node)
const children = createChildren(state, node)
addNode(state, props, type, node)
addChildren(props, children)
// Restore.
state.ancestors.pop()
state.schema = parentSchema
return state.create(node, type, props, key)
}
/**
* Handle root.
*
* @param {State} state
* Info passed around.
* @param {Root} node
* Current node.
* @param {string | undefined} key
* Key.
* @returns {Child | undefined}
* Child, optional.
*/
function root(state, node, key) {
/** @type {Props} */
const props = {}
addChildren(props, createChildren(state, node))
return state.create(node, state.Fragment, props, key)
}
/**
* Handle text.
*
* @param {State} _
* Info passed around.
* @param {Text} node
* Current node.
* @returns {Child | undefined}
* Child, optional.
*/
function text(_, node) {
return node.value
}
/**
* Add `node` to props.
*
* @param {State} state
* Info passed around.
* @param {Props} props
* Props.
* @param {unknown} type
* Type.
* @param {Element | MdxJsxFlowElementHast | MdxJsxTextElementHast} node
* Node.
* @returns {undefined}
* Nothing.
*/
function addNode(state, props, type, node) {
// If this is swapped out for a component:
if (typeof type !== 'string' && type !== state.Fragment && state.passNode) {
props.node = node
}
}
/**
* Add children to props.
*
* @param {Props} props
* Props.
* @param {Array<Child>} children
* Children.
* @returns {undefined}
* Nothing.
*/
function addChildren(props, children) {
if (children.length > 0) {
const value = children.length > 1 ? children : children[0]
if (value) {
props.children = value
}
}
}
/**
* @param {string | undefined} _
* Path to file.
* @param {Jsx} jsx
* Dynamic.
* @param {Jsx} jsxs
* Static.
* @returns {Create}
* Create a production element.
*/
function productionCreate(_, jsx, jsxs) {
return create
/** @type {Create} */
function create(_, type, props, key) {
// Only an array when there are 2 or more children.
const isStaticChildren = Array.isArray(props.children)
const fn = isStaticChildren ? jsxs : jsx
return key ? fn(type, props, key) : fn(type, props)
}
}
/**
* @param {string | undefined} filePath
* Path to file.
* @param {JsxDev} jsxDEV
* Development.
* @returns {Create}
* Create a development element.
*/
function developmentCreate(filePath, jsxDEV) {
return create
/** @type {Create} */
function create(node, type, props, key) {
// Only an array when there are 2 or more children.
const isStaticChildren = Array.isArray(props.children)
const point = pointStart(node)
return jsxDEV(
type,
props,
key,
isStaticChildren,
{
columnNumber: point ? point.column - 1 : undefined,
fileName: filePath,
lineNumber: point ? point.line : undefined
},
undefined
)
}
}
/**
* Create props from an element.
*
* @param {State} state
* Info passed around.
* @param {Element} node
* Current element.
* @returns {Props}
* Props.
*/
function createElementProps(state, node) {
/** @type {Props} */
const props = {}
/** @type {string | undefined} */
let alignValue
/** @type {string} */
let prop
for (prop in node.properties) {
if (prop !== 'children' && own.call(node.properties, prop)) {
const result = createProperty(state, prop, node.properties[prop])
if (result) {
const [key, value] = result
if (
state.tableCellAlignToStyle &&
key === 'align' &&
typeof value === 'string' &&
tableCellElement.has(node.tagName)
) {
alignValue = value
} else {
props[key] = value
}
}
}
}
if (alignValue) {
// Assume style is an object.
const style = /** @type {Style} */ (props.style || (props.style = {}))
style[state.stylePropertyNameCase === 'css' ? 'text-align' : 'textAlign'] =
alignValue
}
return props
}
/**
* Create props from a JSX element.
*
* @param {State} state
* Info passed around.
* @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node
* Current JSX element.
* @returns {Props}
* Props.
*/
function createJsxElementProps(state, node) {
/** @type {Props} */
const props = {}
for (const attribute of node.attributes) {
if (attribute.type === 'mdxJsxExpressionAttribute') {
if (attribute.data && attribute.data.estree && state.evaluater) {
const program = attribute.data.estree
const expression = program.body[0]
assert(expression.type === 'ExpressionStatement')
const objectExpression = expression.expression
assert(objectExpression.type === 'ObjectExpression')
const property = objectExpression.properties[0]
assert(property.type === 'SpreadElement')
Object.assign(
props,
state.evaluater.evaluateExpression(property.argument)
)
} else {
crashEstree(state, node.position)
}
} else {
// For JSX, the author is responsible of passing in the correct values.
const name = attribute.name
/** @type {unknown} */
let value
if (attribute.value && typeof attribute.value === 'object') {
if (
attribute.value.data &&
attribute.value.data.estree &&
state.evaluater
) {
const program = attribute.value.data.estree
const expression = program.body[0]
assert(expression.type === 'ExpressionStatement')
value = state.evaluater.evaluateExpression(expression.expression)
} else {
crashEstree(state, node.position)
}
} else {
value = attribute.value === null ? true : attribute.value
}
// Assume a prop.
props[name] = /** @type {Props[keyof Props]} */ (value)
}
}
return props
}
/**
* Create children.
*
* @param {State} state
* Info passed around.
* @param {Parents} node
* Current element.
* @returns {Array<Child>}
* Children.
*/
function createChildren(state, node) {
/** @type {Array<Child>} */
const children = []
let index = -1
/** @type {Map<string, number>} */
// Note: test this when Solid doesnt want to merge my upcoming PR.
/* c8 ignore next */
const countsByName = state.passKeys ? new Map() : emptyMap
while (++index < node.children.length) {
const child = node.children[index]
/** @type {string | undefined} */
let key
if (state.passKeys) {
const name =
child.type === 'element'
? child.tagName
: child.type === 'mdxJsxFlowElement' ||
child.type === 'mdxJsxTextElement'
? child.name
: undefined
if (name) {
const count = countsByName.get(name) || 0
key = name + '-' + count
countsByName.set(name, count + 1)
}
}
const result = one(state, child, key)
if (result !== undefined) children.push(result)
}
return children
}
/**
* Handle a property.
*
* @param {State} state
* Info passed around.
* @param {string} prop
* Key.
* @param {Array<number | string> | boolean | number | string | null | undefined} value
* hast property value.
* @returns {Field | undefined}
* Field for runtime, optional.
*/
function createProperty(state, prop, value) {
const info = find(state.schema, prop)
// Ignore nullish and `NaN` values.
if (
value === null ||
value === undefined ||
(typeof value === 'number' && Number.isNaN(value))
) {
return
}
if (Array.isArray(value)) {
// Accept `array`.
// Most props are space-separated.
value = info.commaSeparated ? commas(value) : spaces(value)
}
// React only accepts `style` as object.
if (info.property === 'style') {
let styleObject =
typeof value === 'object' ? value : parseStyle(state, String(value))
if (state.stylePropertyNameCase === 'css') {
styleObject = transformStylesToCssCasing(styleObject)
}
return ['style', styleObject]
}
return [
state.elementAttributeNameCase === 'react' && info.space
? hastToReact[info.property] || info.property
: info.attribute,
value
]
}
/**
* Parse a CSS declaration to an object.
*
* @param {State} state
* Info passed around.
* @param {string} value
* CSS declarations.
* @returns {Style}
* Properties.
* @throws
* Throws `VFileMessage` when CSS cannot be parsed.
*/
function parseStyle(state, value) {
try {
return styleToJs(value, {reactCompat: true})
} catch (error) {
if (state.ignoreInvalidStyle) {
return {}
}
const cause = /** @type {Error} */ (error)
const message = new VFileMessage('Cannot parse `style` attribute', {
ancestors: state.ancestors,
cause,
ruleId: 'style',
source: 'hast-util-to-jsx-runtime'
})
message.file = state.filePath || undefined
message.url = docs + '#cannot-parse-style-attribute'
throw message
}
}
/**
* Create a JSX name from a string.
*
* @param {State} state
* To do.
* @param {string} name
* Name.
* @param {boolean} allowExpression
* Allow member expressions and identifiers.
* @returns {unknown}
* To do.
*/
function findComponentFromName(state, name, allowExpression) {
/** @type {Identifier | Literal | MemberExpression} */
let result
if (!allowExpression) {
result = {type: 'Literal', value: name}
} else if (name.includes('.')) {
const identifiers = name.split('.')
let index = -1
/** @type {Identifier | Literal | MemberExpression | undefined} */
let node
while (++index < identifiers.length) {
/** @type {Identifier | Literal} */
const prop = isIdentifierName(identifiers[index])
? {type: 'Identifier', name: identifiers[index]}
: {type: 'Literal', value: identifiers[index]}
node = node
? {
type: 'MemberExpression',
object: node,
property: prop,
computed: Boolean(index && prop.type === 'Literal'),
optional: false
}
: prop
}
assert(node, 'always a result')
result = node
} else {
result =
isIdentifierName(name) && !/^[a-z]/.test(name)
? {type: 'Identifier', name}
: {type: 'Literal', value: name}
}
// Only literals can be passed in `components` currently.
// No identifiers / member expressions.
if (result.type === 'Literal') {
const name = /** @type {string | number} */ (result.value)
return own.call(state.components, name) ? state.components[name] : name
}
// Assume component.
if (state.evaluater) {
return state.evaluater.evaluateExpression(result)
}
crashEstree(state)
}
/**
* @param {State} state
* @param {Position | undefined} [place]
* @returns {never}
*/
function crashEstree(state, place) {
const message = new VFileMessage(
'Cannot handle MDX estrees without `createEvaluater`',
{
ancestors: state.ancestors,
place,
ruleId: 'mdx-estree',
source: 'hast-util-to-jsx-runtime'
}
)
message.file = state.filePath || undefined
message.url = docs + '#cannot-handle-mdx-estrees-without-createevaluater'
throw message
}
/**
* Transform a DOM casing style object to a CSS casing style object.
*
* @param {Style} domCasing
* @returns {Style}
*/
function transformStylesToCssCasing(domCasing) {
/** @type {Style} */
const cssCasing = {}
/** @type {string} */
let from
for (from in domCasing) {
if (own.call(domCasing, from)) {
cssCasing[transformStyleToCssCasing(from)] = domCasing[from]
}
}
return cssCasing
}
/**
* Transform a DOM casing style field to a CSS casing style field.
*
* @param {string} from
* @returns {string}
*/
function transformStyleToCssCasing(from) {
let to = from.replace(cap, toDash)
// Handle `ms-xxx` -> `-ms-xxx`.
if (to.slice(0, 3) === 'ms-') to = '-' + to
return to
}
/**
* Make `$0` dash cased.
*
* @param {string} $0
* Capitalized ASCII leter.
* @returns {string}
* Dash and lower letter.
*/
function toDash($0) {
return '-' + $0.toLowerCase()
}
@@ -0,0 +1,34 @@
/**
* @typedef {import('hast').Nodes} Nodes
*/
// HTML whitespace expression.
// See <https://infra.spec.whatwg.org/#ascii-whitespace>.
const re = /[ \t\n\f\r]/g
/**
* Check if the given value is *inter-element whitespace*.
*
* @param {Nodes | string} thing
* Thing to check (`Node` or `string`).
* @returns {boolean}
* Whether the `value` is inter-element whitespace (`boolean`): consisting of
* zero or more of space, tab (`\t`), line feed (`\n`), carriage return
* (`\r`), or form feed (`\f`); if a node is passed it must be a `Text` node,
* whose `value` field is checked.
*/
export function whitespace(thing) {
return typeof thing === 'object'
? thing.type === 'text'
? empty(thing.value)
: false
: empty(thing)
}
/**
* @param {string} value
* @returns {boolean}
*/
function empty(value) {
return value.replace(re, '') === ''
}
@@ -0,0 +1,17 @@
// Note: types exposed from `index.d.ts`.
import {merge} from './lib/util/merge.js'
import {aria} from './lib/aria.js'
import {html as htmlBase} from './lib/html.js'
import {svg as svgBase} from './lib/svg.js'
import {xlink} from './lib/xlink.js'
import {xmlns} from './lib/xmlns.js'
import {xml} from './lib/xml.js'
export {hastToReact} from './lib/hast-to-react.js'
export const html = merge([aria, htmlBase, xlink, xmlns, xml], 'html')
export {find} from './lib/find.js'
export {normalize} from './lib/normalize.js'
export const svg = merge([aria, svgBase, xlink, xmlns, xml], 'svg')
@@ -0,0 +1,61 @@
import {create} from './util/create.js'
import {booleanish, number, spaceSeparated} from './util/types.js'
export const aria = create({
properties: {
ariaActiveDescendant: null,
ariaAtomic: booleanish,
ariaAutoComplete: null,
ariaBusy: booleanish,
ariaChecked: booleanish,
ariaColCount: number,
ariaColIndex: number,
ariaColSpan: number,
ariaControls: spaceSeparated,
ariaCurrent: null,
ariaDescribedBy: spaceSeparated,
ariaDetails: null,
ariaDisabled: booleanish,
ariaDropEffect: spaceSeparated,
ariaErrorMessage: null,
ariaExpanded: booleanish,
ariaFlowTo: spaceSeparated,
ariaGrabbed: booleanish,
ariaHasPopup: null,
ariaHidden: booleanish,
ariaInvalid: null,
ariaKeyShortcuts: null,
ariaLabel: null,
ariaLabelledBy: spaceSeparated,
ariaLevel: number,
ariaLive: null,
ariaModal: booleanish,
ariaMultiLine: booleanish,
ariaMultiSelectable: booleanish,
ariaOrientation: null,
ariaOwns: spaceSeparated,
ariaPlaceholder: null,
ariaPosInSet: number,
ariaPressed: booleanish,
ariaReadOnly: booleanish,
ariaRelevant: null,
ariaRequired: booleanish,
ariaRoleDescription: spaceSeparated,
ariaRowCount: number,
ariaRowIndex: number,
ariaRowSpan: number,
ariaSelected: booleanish,
ariaSetSize: number,
ariaSort: null,
ariaValueMax: number,
ariaValueMin: number,
ariaValueNow: number,
ariaValueText: null,
role: null
},
transform(_, property) {
return property === 'role'
? property
: 'aria-' + property.slice(4).toLowerCase()
}
})
@@ -0,0 +1,97 @@
/**
* @import {Schema} from 'property-information'
*/
import {DefinedInfo} from './util/defined-info.js'
import {Info} from './util/info.js'
import {normalize} from './normalize.js'
const cap = /[A-Z]/g
const dash = /-[a-z]/g
const valid = /^data[-\w.:]+$/i
/**
* Look up info on a property.
*
* In most cases the given `schema` contains info on the property.
* All standard,
* most legacy,
* and some non-standard properties are supported.
* For these cases,
* the returned `Info` has hints about the value of the property.
*
* `name` can also be a valid data attribute or property,
* in which case an `Info` object with the correctly cased `attribute` and
* `property` is returned.
*
* `name` can be an unknown attribute,
* in which case an `Info` object with `attribute` and `property` set to the
* given name is returned.
* It is not recommended to provide unsupported legacy or recently specced
* properties.
*
*
* @param {Schema} schema
* Schema;
* either the `html` or `svg` export.
* @param {string} value
* An attribute-like or property-like name;
* it will be passed through `normalize` to hopefully find the correct info.
* @returns {Info}
* Info.
*/
export function find(schema, value) {
const normal = normalize(value)
let property = value
let Type = Info
if (normal in schema.normal) {
return schema.property[schema.normal[normal]]
}
if (normal.length > 4 && normal.slice(0, 4) === 'data' && valid.test(value)) {
// Attribute or property.
if (value.charAt(4) === '-') {
// Turn it into a property.
const rest = value.slice(5).replace(dash, camelcase)
property = 'data' + rest.charAt(0).toUpperCase() + rest.slice(1)
} else {
// Turn it into an attribute.
const rest = value.slice(4)
if (!dash.test(rest)) {
let dashes = rest.replace(cap, kebab)
if (dashes.charAt(0) !== '-') {
dashes = '-' + dashes
}
value = 'data' + dashes
}
}
Type = DefinedInfo
}
return new Type(property, value)
}
/**
* @param {string} $0
* Value.
* @returns {string}
* Kebab.
*/
function kebab($0) {
return '-' + $0.toLowerCase()
}
/**
* @param {string} $0
* Value.
* @returns {string}
* Camel.
*/
function camelcase($0) {
return $0.charAt(1).toUpperCase()
}
@@ -0,0 +1,30 @@
/**
* Special cases for React (`Record<string, string>`).
*
* `hast` is close to `React` but differs in a couple of cases.
* To get a React property from a hast property,
* check if it is in `hastToReact`.
* If it is, use the corresponding value;
* otherwise, use the hast property.
*
* @type {Record<string, string>}
*/
export const hastToReact = {
classId: 'classID',
dataType: 'datatype',
itemId: 'itemID',
strokeDashArray: 'strokeDasharray',
strokeDashOffset: 'strokeDashoffset',
strokeLineCap: 'strokeLinecap',
strokeLineJoin: 'strokeLinejoin',
strokeMiterLimit: 'strokeMiterlimit',
typeOf: 'typeof',
xLinkActuate: 'xlinkActuate',
xLinkArcRole: 'xlinkArcrole',
xLinkHref: 'xlinkHref',
xLinkRole: 'xlinkRole',
xLinkShow: 'xlinkShow',
xLinkTitle: 'xlinkTitle',
xLinkType: 'xlinkType',
xmlnsXLink: 'xmlnsXlink'
}
@@ -0,0 +1,332 @@
import {caseInsensitiveTransform} from './util/case-insensitive-transform.js'
import {create} from './util/create.js'
import {
booleanish,
boolean,
commaSeparated,
number,
overloadedBoolean,
spaceSeparated
} from './util/types.js'
export const html = create({
attributes: {
acceptcharset: 'accept-charset',
classname: 'class',
htmlfor: 'for',
httpequiv: 'http-equiv'
},
mustUseProperty: ['checked', 'multiple', 'muted', 'selected'],
properties: {
// Standard Properties.
abbr: null,
accept: commaSeparated,
acceptCharset: spaceSeparated,
accessKey: spaceSeparated,
action: null,
allow: null,
allowFullScreen: boolean,
allowPaymentRequest: boolean,
allowUserMedia: boolean,
alpha: boolean,
alt: null,
as: null,
async: boolean,
autoCapitalize: null,
autoComplete: spaceSeparated,
autoFocus: boolean,
autoPlay: boolean,
blocking: spaceSeparated,
capture: null,
charSet: null,
checked: boolean,
cite: null,
className: spaceSeparated,
closedBy: null,
colorSpace: null,
cols: number,
colSpan: number,
command: null,
commandFor: null,
content: null,
contentEditable: booleanish,
controls: boolean,
controlsList: spaceSeparated,
coords: number | commaSeparated,
crossOrigin: null,
data: null,
dateTime: null,
decoding: null,
default: boolean,
defer: boolean,
dir: null,
dirName: null,
disabled: boolean,
download: overloadedBoolean,
draggable: booleanish,
encType: null,
enterKeyHint: null,
fetchPriority: null,
form: null,
formAction: null,
formEncType: null,
formMethod: null,
formNoValidate: boolean,
formTarget: null,
headers: spaceSeparated,
height: number,
hidden: overloadedBoolean,
high: number,
href: null,
hrefLang: null,
htmlFor: spaceSeparated,
httpEquiv: spaceSeparated,
id: null,
imageSizes: null,
imageSrcSet: null,
inert: boolean,
inputMode: null,
integrity: null,
is: null,
isMap: boolean,
itemId: null,
itemProp: spaceSeparated,
itemRef: spaceSeparated,
itemScope: boolean,
itemType: spaceSeparated,
kind: null,
label: null,
lang: null,
language: null,
list: null,
loading: null,
loop: boolean,
low: number,
manifest: null,
max: null,
maxLength: number,
media: null,
method: null,
min: null,
minLength: number,
multiple: boolean,
muted: boolean,
name: null,
nonce: null,
noModule: boolean,
noValidate: boolean,
onAbort: null,
onAfterPrint: null,
onAuxClick: null,
onBeforeMatch: null,
onBeforePrint: null,
onBeforeToggle: null,
onBeforeUnload: null,
onBlur: null,
onCancel: null,
onCanPlay: null,
onCanPlayThrough: null,
onChange: null,
onClick: null,
onClose: null,
onContextLost: null,
onContextMenu: null,
onContextRestored: null,
onCopy: null,
onCueChange: null,
onCut: null,
onDblClick: null,
onDrag: null,
onDragEnd: null,
onDragEnter: null,
onDragExit: null,
onDragLeave: null,
onDragOver: null,
onDragStart: null,
onDrop: null,
onDurationChange: null,
onEmptied: null,
onEnded: null,
onError: null,
onFocus: null,
onFormData: null,
onHashChange: null,
onInput: null,
onInvalid: null,
onKeyDown: null,
onKeyPress: null,
onKeyUp: null,
onLanguageChange: null,
onLoad: null,
onLoadedData: null,
onLoadedMetadata: null,
onLoadEnd: null,
onLoadStart: null,
onMessage: null,
onMessageError: null,
onMouseDown: null,
onMouseEnter: null,
onMouseLeave: null,
onMouseMove: null,
onMouseOut: null,
onMouseOver: null,
onMouseUp: null,
onOffline: null,
onOnline: null,
onPageHide: null,
onPageShow: null,
onPaste: null,
onPause: null,
onPlay: null,
onPlaying: null,
onPopState: null,
onProgress: null,
onRateChange: null,
onRejectionHandled: null,
onReset: null,
onResize: null,
onScroll: null,
onScrollEnd: null,
onSecurityPolicyViolation: null,
onSeeked: null,
onSeeking: null,
onSelect: null,
onSlotChange: null,
onStalled: null,
onStorage: null,
onSubmit: null,
onSuspend: null,
onTimeUpdate: null,
onToggle: null,
onUnhandledRejection: null,
onUnload: null,
onVolumeChange: null,
onWaiting: null,
onWheel: null,
open: boolean,
optimum: number,
pattern: null,
ping: spaceSeparated,
placeholder: null,
playsInline: boolean,
popover: null,
popoverTarget: null,
popoverTargetAction: null,
poster: null,
preload: null,
readOnly: boolean,
referrerPolicy: null,
rel: spaceSeparated,
required: boolean,
reversed: boolean,
rows: number,
rowSpan: number,
sandbox: spaceSeparated,
scope: null,
scoped: boolean,
seamless: boolean,
selected: boolean,
shadowRootClonable: boolean,
shadowRootCustomElementRegistry: boolean,
shadowRootDelegatesFocus: boolean,
shadowRootMode: null,
shadowRootSerializable: boolean,
shape: null,
size: number,
sizes: null,
slot: null,
span: number,
spellCheck: booleanish,
src: null,
srcDoc: null,
srcLang: null,
srcSet: null,
start: number,
step: null,
style: null,
tabIndex: number,
target: null,
title: null,
translate: null,
type: null,
typeMustMatch: boolean,
useMap: null,
value: booleanish,
width: number,
wrap: null,
writingSuggestions: null,
// Legacy.
// See: https://html.spec.whatwg.org/#other-elements,-attributes-and-apis
align: null, // Several. Use CSS `text-align` instead,
aLink: null, // `<body>`. Use CSS `a:active {color}` instead
archive: spaceSeparated, // `<object>`. List of URIs to archives
axis: null, // `<td>` and `<th>`. Use `scope` on `<th>`
background: null, // `<body>`. Use CSS `background-image` instead
bgColor: null, // `<body>` and table elements. Use CSS `background-color` instead
border: number, // `<table>`. Use CSS `border-width` instead,
borderColor: null, // `<table>`. Use CSS `border-color` instead,
bottomMargin: number, // `<body>`
cellPadding: null, // `<table>`
cellSpacing: null, // `<table>`
char: null, // Several table elements. When `align=char`, sets the character to align on
charOff: null, // Several table elements. When `char`, offsets the alignment
classId: null, // `<object>`
clear: null, // `<br>`. Use CSS `clear` instead
code: null, // `<object>`
codeBase: null, // `<object>`
codeType: null, // `<object>`
color: null, // `<font>` and `<hr>`. Use CSS instead
compact: boolean, // Lists. Use CSS to reduce space between items instead
declare: boolean, // `<object>`
event: null, // `<script>`
face: null, // `<font>`. Use CSS instead
frame: null, // `<table>`
frameBorder: null, // `<iframe>`. Use CSS `border` instead
hSpace: number, // `<img>` and `<object>`
leftMargin: number, // `<body>`
link: null, // `<body>`. Use CSS `a:link {color: *}` instead
longDesc: null, // `<frame>`, `<iframe>`, and `<img>`. Use an `<a>`
lowSrc: null, // `<img>`. Use a `<picture>`
marginHeight: number, // `<body>`
marginWidth: number, // `<body>`
noResize: boolean, // `<frame>`
noHref: boolean, // `<area>`. Use no href instead of an explicit `nohref`
noShade: boolean, // `<hr>`. Use background-color and height instead of borders
noWrap: boolean, // `<td>` and `<th>`
object: null, // `<applet>`
profile: null, // `<head>`
prompt: null, // `<isindex>`
rev: null, // `<link>`
rightMargin: number, // `<body>`
rules: null, // `<table>`
scheme: null, // `<meta>`
scrolling: booleanish, // `<frame>`. Use overflow in the child context
standby: null, // `<object>`
summary: null, // `<table>`
text: null, // `<body>`. Use CSS `color` instead
topMargin: number, // `<body>`
valueType: null, // `<param>`
version: null, // `<html>`. Use a doctype.
vAlign: null, // Several. Use CSS `vertical-align` instead
vLink: null, // `<body>`. Use CSS `a:visited {color}` instead
vSpace: number, // `<img>` and `<object>`
// Non-standard Properties.
allowTransparency: null,
autoCorrect: null,
autoSave: null,
credentialless: boolean,
disablePictureInPicture: boolean,
disableRemotePlayback: boolean,
exportParts: commaSeparated,
part: spaceSeparated,
prefix: null,
property: null,
results: number,
security: null,
unselectable: null
},
space: 'html',
transform: caseInsensitiveTransform
})
@@ -0,0 +1,12 @@
/**
* Get the cleaned case insensitive form of an attribute or property.
*
* @param {string} value
* An attribute-like or property-like name.
* @returns {string}
* Value that can be used to look up the properly cased property on a
* `Schema`.
*/
export function normalize(value) {
return value.toLowerCase()
}
@@ -0,0 +1,569 @@
import {caseSensitiveTransform} from './util/case-sensitive-transform.js'
import {create} from './util/create.js'
import {
boolean,
commaOrSpaceSeparated,
commaSeparated,
number,
spaceSeparated
} from './util/types.js'
export const svg = create({
attributes: {
accentHeight: 'accent-height',
alignmentBaseline: 'alignment-baseline',
arabicForm: 'arabic-form',
baselineShift: 'baseline-shift',
capHeight: 'cap-height',
className: 'class',
clipPath: 'clip-path',
clipRule: 'clip-rule',
colorInterpolation: 'color-interpolation',
colorInterpolationFilters: 'color-interpolation-filters',
colorProfile: 'color-profile',
colorRendering: 'color-rendering',
crossOrigin: 'crossorigin',
dataType: 'datatype',
dominantBaseline: 'dominant-baseline',
enableBackground: 'enable-background',
fillOpacity: 'fill-opacity',
fillRule: 'fill-rule',
floodColor: 'flood-color',
floodOpacity: 'flood-opacity',
fontFamily: 'font-family',
fontSize: 'font-size',
fontSizeAdjust: 'font-size-adjust',
fontStretch: 'font-stretch',
fontStyle: 'font-style',
fontVariant: 'font-variant',
fontWeight: 'font-weight',
glyphName: 'glyph-name',
glyphOrientationHorizontal: 'glyph-orientation-horizontal',
glyphOrientationVertical: 'glyph-orientation-vertical',
hrefLang: 'hreflang',
horizAdvX: 'horiz-adv-x',
horizOriginX: 'horiz-origin-x',
horizOriginY: 'horiz-origin-y',
imageRendering: 'image-rendering',
letterSpacing: 'letter-spacing',
lightingColor: 'lighting-color',
markerEnd: 'marker-end',
markerMid: 'marker-mid',
markerStart: 'marker-start',
maskType: 'mask-type',
navDown: 'nav-down',
navDownLeft: 'nav-down-left',
navDownRight: 'nav-down-right',
navLeft: 'nav-left',
navNext: 'nav-next',
navPrev: 'nav-prev',
navRight: 'nav-right',
navUp: 'nav-up',
navUpLeft: 'nav-up-left',
navUpRight: 'nav-up-right',
onAbort: 'onabort',
onActivate: 'onactivate',
onAfterPrint: 'onafterprint',
onBeforePrint: 'onbeforeprint',
onBegin: 'onbegin',
onCancel: 'oncancel',
onCanPlay: 'oncanplay',
onCanPlayThrough: 'oncanplaythrough',
onChange: 'onchange',
onClick: 'onclick',
onClose: 'onclose',
onCopy: 'oncopy',
onCueChange: 'oncuechange',
onCut: 'oncut',
onDblClick: 'ondblclick',
onDrag: 'ondrag',
onDragEnd: 'ondragend',
onDragEnter: 'ondragenter',
onDragExit: 'ondragexit',
onDragLeave: 'ondragleave',
onDragOver: 'ondragover',
onDragStart: 'ondragstart',
onDrop: 'ondrop',
onDurationChange: 'ondurationchange',
onEmptied: 'onemptied',
onEnd: 'onend',
onEnded: 'onended',
onError: 'onerror',
onFocus: 'onfocus',
onFocusIn: 'onfocusin',
onFocusOut: 'onfocusout',
onHashChange: 'onhashchange',
onInput: 'oninput',
onInvalid: 'oninvalid',
onKeyDown: 'onkeydown',
onKeyPress: 'onkeypress',
onKeyUp: 'onkeyup',
onLoad: 'onload',
onLoadedData: 'onloadeddata',
onLoadedMetadata: 'onloadedmetadata',
onLoadStart: 'onloadstart',
onMessage: 'onmessage',
onMouseDown: 'onmousedown',
onMouseEnter: 'onmouseenter',
onMouseLeave: 'onmouseleave',
onMouseMove: 'onmousemove',
onMouseOut: 'onmouseout',
onMouseOver: 'onmouseover',
onMouseUp: 'onmouseup',
onMouseWheel: 'onmousewheel',
onOffline: 'onoffline',
onOnline: 'ononline',
onPageHide: 'onpagehide',
onPageShow: 'onpageshow',
onPaste: 'onpaste',
onPause: 'onpause',
onPlay: 'onplay',
onPlaying: 'onplaying',
onPopState: 'onpopstate',
onProgress: 'onprogress',
onRateChange: 'onratechange',
onRepeat: 'onrepeat',
onReset: 'onreset',
onResize: 'onresize',
onScroll: 'onscroll',
onSeeked: 'onseeked',
onSeeking: 'onseeking',
onSelect: 'onselect',
onShow: 'onshow',
onStalled: 'onstalled',
onStorage: 'onstorage',
onSubmit: 'onsubmit',
onSuspend: 'onsuspend',
onTimeUpdate: 'ontimeupdate',
onToggle: 'ontoggle',
onUnload: 'onunload',
onVolumeChange: 'onvolumechange',
onWaiting: 'onwaiting',
onZoom: 'onzoom',
overlinePosition: 'overline-position',
overlineThickness: 'overline-thickness',
paintOrder: 'paint-order',
panose1: 'panose-1',
pointerEvents: 'pointer-events',
referrerPolicy: 'referrerpolicy',
renderingIntent: 'rendering-intent',
shapeRendering: 'shape-rendering',
stopColor: 'stop-color',
stopOpacity: 'stop-opacity',
strikethroughPosition: 'strikethrough-position',
strikethroughThickness: 'strikethrough-thickness',
strokeDashArray: 'stroke-dasharray',
strokeDashOffset: 'stroke-dashoffset',
strokeLineCap: 'stroke-linecap',
strokeLineJoin: 'stroke-linejoin',
strokeMiterLimit: 'stroke-miterlimit',
strokeOpacity: 'stroke-opacity',
strokeWidth: 'stroke-width',
tabIndex: 'tabindex',
textAnchor: 'text-anchor',
textDecoration: 'text-decoration',
textRendering: 'text-rendering',
transformOrigin: 'transform-origin',
typeOf: 'typeof',
underlinePosition: 'underline-position',
underlineThickness: 'underline-thickness',
unicodeBidi: 'unicode-bidi',
unicodeRange: 'unicode-range',
unitsPerEm: 'units-per-em',
vAlphabetic: 'v-alphabetic',
vHanging: 'v-hanging',
vIdeographic: 'v-ideographic',
vMathematical: 'v-mathematical',
vectorEffect: 'vector-effect',
vertAdvY: 'vert-adv-y',
vertOriginX: 'vert-origin-x',
vertOriginY: 'vert-origin-y',
wordSpacing: 'word-spacing',
writingMode: 'writing-mode',
xHeight: 'x-height',
// These were camelcased in Tiny. Now lowercased in SVG 2
playbackOrder: 'playbackorder',
timelineBegin: 'timelinebegin'
},
properties: {
about: commaOrSpaceSeparated,
accentHeight: number,
accumulate: null,
additive: null,
alignmentBaseline: null,
alphabetic: number,
amplitude: number,
arabicForm: null,
ascent: number,
attributeName: null,
attributeType: null,
azimuth: number,
bandwidth: null,
baselineShift: null,
baseFrequency: null,
baseProfile: null,
bbox: null,
begin: null,
bias: number,
by: null,
calcMode: null,
capHeight: number,
className: spaceSeparated,
clip: null,
clipPath: null,
clipPathUnits: null,
clipRule: null,
color: null,
colorInterpolation: null,
colorInterpolationFilters: null,
colorProfile: null,
colorRendering: null,
content: null,
contentScriptType: null,
contentStyleType: null,
crossOrigin: null,
cursor: null,
cx: null,
cy: null,
d: null,
dataType: null,
defaultAction: null,
descent: number,
diffuseConstant: number,
direction: null,
display: null,
dur: null,
divisor: number,
dominantBaseline: null,
download: boolean,
dx: null,
dy: null,
edgeMode: null,
editable: null,
elevation: number,
enableBackground: null,
end: null,
event: null,
exponent: number,
externalResourcesRequired: null,
fill: null,
fillOpacity: number,
fillRule: null,
filter: null,
filterRes: null,
filterUnits: null,
floodColor: null,
floodOpacity: null,
focusable: null,
focusHighlight: null,
fontFamily: null,
fontSize: null,
fontSizeAdjust: null,
fontStretch: null,
fontStyle: null,
fontVariant: null,
fontWeight: null,
format: null,
fr: null,
from: null,
fx: null,
fy: null,
g1: commaSeparated,
g2: commaSeparated,
glyphName: commaSeparated,
glyphOrientationHorizontal: null,
glyphOrientationVertical: null,
glyphRef: null,
gradientTransform: null,
gradientUnits: null,
handler: null,
hanging: number,
hatchContentUnits: null,
hatchUnits: null,
height: null,
href: null,
hrefLang: null,
horizAdvX: number,
horizOriginX: number,
horizOriginY: number,
id: null,
ideographic: number,
imageRendering: null,
initialVisibility: null,
in: null,
in2: null,
intercept: number,
k: number,
k1: number,
k2: number,
k3: number,
k4: number,
kernelMatrix: commaOrSpaceSeparated,
kernelUnitLength: null,
keyPoints: null, // SEMI_COLON_SEPARATED
keySplines: null, // SEMI_COLON_SEPARATED
keyTimes: null, // SEMI_COLON_SEPARATED
kerning: null,
lang: null,
lengthAdjust: null,
letterSpacing: null,
lightingColor: null,
limitingConeAngle: number,
local: null,
markerEnd: null,
markerMid: null,
markerStart: null,
markerHeight: null,
markerUnits: null,
markerWidth: null,
mask: null,
maskContentUnits: null,
maskType: null,
maskUnits: null,
mathematical: null,
max: null,
media: null,
mediaCharacterEncoding: null,
mediaContentEncodings: null,
mediaSize: number,
mediaTime: null,
method: null,
min: null,
mode: null,
name: null,
navDown: null,
navDownLeft: null,
navDownRight: null,
navLeft: null,
navNext: null,
navPrev: null,
navRight: null,
navUp: null,
navUpLeft: null,
navUpRight: null,
numOctaves: null,
observer: null,
offset: null,
onAbort: null,
onActivate: null,
onAfterPrint: null,
onBeforePrint: null,
onBegin: null,
onCancel: null,
onCanPlay: null,
onCanPlayThrough: null,
onChange: null,
onClick: null,
onClose: null,
onCopy: null,
onCueChange: null,
onCut: null,
onDblClick: null,
onDrag: null,
onDragEnd: null,
onDragEnter: null,
onDragExit: null,
onDragLeave: null,
onDragOver: null,
onDragStart: null,
onDrop: null,
onDurationChange: null,
onEmptied: null,
onEnd: null,
onEnded: null,
onError: null,
onFocus: null,
onFocusIn: null,
onFocusOut: null,
onHashChange: null,
onInput: null,
onInvalid: null,
onKeyDown: null,
onKeyPress: null,
onKeyUp: null,
onLoad: null,
onLoadedData: null,
onLoadedMetadata: null,
onLoadStart: null,
onMessage: null,
onMouseDown: null,
onMouseEnter: null,
onMouseLeave: null,
onMouseMove: null,
onMouseOut: null,
onMouseOver: null,
onMouseUp: null,
onMouseWheel: null,
onOffline: null,
onOnline: null,
onPageHide: null,
onPageShow: null,
onPaste: null,
onPause: null,
onPlay: null,
onPlaying: null,
onPopState: null,
onProgress: null,
onRateChange: null,
onRepeat: null,
onReset: null,
onResize: null,
onScroll: null,
onSeeked: null,
onSeeking: null,
onSelect: null,
onShow: null,
onStalled: null,
onStorage: null,
onSubmit: null,
onSuspend: null,
onTimeUpdate: null,
onToggle: null,
onUnload: null,
onVolumeChange: null,
onWaiting: null,
onZoom: null,
opacity: null,
operator: null,
order: null,
orient: null,
orientation: null,
origin: null,
overflow: null,
overlay: null,
overlinePosition: number,
overlineThickness: number,
paintOrder: null,
panose1: null,
path: null,
pathLength: number,
patternContentUnits: null,
patternTransform: null,
patternUnits: null,
phase: null,
ping: spaceSeparated,
pitch: null,
playbackOrder: null,
pointerEvents: null,
points: null,
pointsAtX: number,
pointsAtY: number,
pointsAtZ: number,
preserveAlpha: null,
preserveAspectRatio: null,
primitiveUnits: null,
propagate: null,
property: commaOrSpaceSeparated,
r: null,
radius: null,
referrerPolicy: null,
refX: null,
refY: null,
rel: commaOrSpaceSeparated,
rev: commaOrSpaceSeparated,
renderingIntent: null,
repeatCount: null,
repeatDur: null,
requiredExtensions: commaOrSpaceSeparated,
requiredFeatures: commaOrSpaceSeparated,
requiredFonts: commaOrSpaceSeparated,
requiredFormats: commaOrSpaceSeparated,
resource: null,
restart: null,
result: null,
rotate: null,
rx: null,
ry: null,
scale: null,
seed: null,
shapeRendering: null,
side: null,
slope: null,
snapshotTime: null,
specularConstant: number,
specularExponent: number,
spreadMethod: null,
spacing: null,
startOffset: null,
stdDeviation: null,
stemh: null,
stemv: null,
stitchTiles: null,
stopColor: null,
stopOpacity: null,
strikethroughPosition: number,
strikethroughThickness: number,
string: null,
stroke: null,
strokeDashArray: commaOrSpaceSeparated,
strokeDashOffset: null,
strokeLineCap: null,
strokeLineJoin: null,
strokeMiterLimit: number,
strokeOpacity: number,
strokeWidth: null,
style: null,
surfaceScale: number,
syncBehavior: null,
syncBehaviorDefault: null,
syncMaster: null,
syncTolerance: null,
syncToleranceDefault: null,
systemLanguage: commaOrSpaceSeparated,
tabIndex: number,
tableValues: null,
target: null,
targetX: number,
targetY: number,
textAnchor: null,
textDecoration: null,
textRendering: null,
textLength: null,
timelineBegin: null,
title: null,
transformBehavior: null,
type: null,
typeOf: commaOrSpaceSeparated,
to: null,
transform: null,
transformOrigin: null,
u1: null,
u2: null,
underlinePosition: number,
underlineThickness: number,
unicode: null,
unicodeBidi: null,
unicodeRange: null,
unitsPerEm: number,
values: null,
vAlphabetic: number,
vMathematical: number,
vectorEffect: null,
vHanging: number,
vIdeographic: number,
version: null,
vertAdvY: number,
vertOriginX: number,
vertOriginY: number,
viewBox: null,
viewTarget: null,
visibility: null,
width: null,
widths: null,
wordSpacing: null,
writingMode: null,
x: null,
x1: null,
x2: null,
xChannelSelector: null,
xHeight: number,
y: null,
y1: null,
y2: null,
yChannelSelector: null,
z: null,
zoomAndPan: null
},
space: 'svg',
transform: caseSensitiveTransform
})
@@ -0,0 +1,13 @@
import {caseSensitiveTransform} from './case-sensitive-transform.js'
/**
* @param {Record<string, string>} attributes
* Attributes.
* @param {string} property
* Property.
* @returns {string}
* Transformed property.
*/
export function caseInsensitiveTransform(attributes, property) {
return caseSensitiveTransform(attributes, property.toLowerCase())
}
@@ -0,0 +1,11 @@
/**
* @param {Record<string, string>} attributes
* Attributes.
* @param {string} attribute
* Attribute.
* @returns {string}
* Transformed attribute.
*/
export function caseSensitiveTransform(attributes, attribute) {
return attribute in attributes ? attributes[attribute] : attribute
}
@@ -0,0 +1,69 @@
/**
* @import {Info, Space} from 'property-information'
*/
/**
* @typedef Definition
* Definition of a schema.
* @property {Record<string, string> | undefined} [attributes]
* Normalzed names to special attribute case.
* @property {ReadonlyArray<string> | undefined} [mustUseProperty]
* Normalized names that must be set as properties.
* @property {Record<string, number | null>} properties
* Property names to their types.
* @property {Space | undefined} [space]
* Space.
* @property {Transform} transform
* Transform a property name.
*/
/**
* @callback Transform
* Transform.
* @param {Record<string, string>} attributes
* Attributes.
* @param {string} property
* Property.
* @returns {string}
* Attribute.
*/
import {normalize} from '../normalize.js'
import {DefinedInfo} from './defined-info.js'
import {Schema} from './schema.js'
/**
* @param {Definition} definition
* Definition.
* @returns {Schema}
* Schema.
*/
export function create(definition) {
/** @type {Record<string, Info>} */
const properties = {}
/** @type {Record<string, string>} */
const normals = {}
for (const [property, value] of Object.entries(definition.properties)) {
const info = new DefinedInfo(
property,
definition.transform(definition.attributes || {}, property),
value,
definition.space
)
if (
definition.mustUseProperty &&
definition.mustUseProperty.includes(property)
) {
info.mustUseProperty = true
}
properties[property] = info
normals[normalize(property)] = property
normals[normalize(info.attribute)] = property
}
return new Schema(properties, normals, definition.space)
}
@@ -0,0 +1,60 @@
/**
* @import {Space} from 'property-information'
*/
import {Info} from './info.js'
import * as types from './types.js'
const checks = /** @type {ReadonlyArray<keyof typeof types>} */ (
Object.keys(types)
)
export class DefinedInfo extends Info {
/**
* @constructor
* @param {string} property
* Property.
* @param {string} attribute
* Attribute.
* @param {number | null | undefined} [mask]
* Mask.
* @param {Space | undefined} [space]
* Space.
* @returns
* Info.
*/
constructor(property, attribute, mask, space) {
let index = -1
super(property, attribute)
mark(this, 'space', space)
if (typeof mask === 'number') {
while (++index < checks.length) {
const check = checks[index]
mark(this, checks[index], (mask & types[check]) === types[check])
}
}
}
}
DefinedInfo.prototype.defined = true
/**
* @template {keyof DefinedInfo} Key
* Key type.
* @param {DefinedInfo} values
* Info.
* @param {Key} key
* Key.
* @param {DefinedInfo[Key]} value
* Value.
* @returns {undefined}
* Nothing.
*/
function mark(values, key, value) {
if (value) {
values[key] = value
}
}
@@ -0,0 +1,32 @@
/**
* @import {Info as InfoType} from 'property-information'
*/
/** @type {InfoType} */
export class Info {
/**
* @param {string} property
* Property.
* @param {string} attribute
* Attribute.
* @returns
* Info.
*/
constructor(property, attribute) {
this.attribute = attribute
this.property = property
}
}
Info.prototype.attribute = ''
Info.prototype.booleanish = false
Info.prototype.boolean = false
Info.prototype.commaOrSpaceSeparated = false
Info.prototype.commaSeparated = false
Info.prototype.defined = false
Info.prototype.mustUseProperty = false
Info.prototype.number = false
Info.prototype.overloadedBoolean = false
Info.prototype.property = ''
Info.prototype.spaceSeparated = false
Info.prototype.space = undefined
@@ -0,0 +1,27 @@
/**
* @import {Info, Space} from 'property-information'
*/
import {Schema} from './schema.js'
/**
* @param {ReadonlyArray<Schema>} definitions
* Definitions.
* @param {Space | undefined} [space]
* Space.
* @returns {Schema}
* Schema.
*/
export function merge(definitions, space) {
/** @type {Record<string, Info>} */
const property = {}
/** @type {Record<string, string>} */
const normal = {}
for (const definition of definitions) {
Object.assign(property, definition.property)
Object.assign(normal, definition.normal)
}
return new Schema(property, normal, space)
}
@@ -0,0 +1,29 @@
/**
* @import {Schema as SchemaType, Space} from 'property-information'
*/
/** @type {SchemaType} */
export class Schema {
/**
* @param {SchemaType['property']} property
* Property.
* @param {SchemaType['normal']} normal
* Normal.
* @param {Space | undefined} [space]
* Space.
* @returns
* Schema.
*/
constructor(property, normal, space) {
this.normal = normal
this.property = property
if (space) {
this.space = space
}
}
}
Schema.prototype.normal = {}
Schema.prototype.property = {}
Schema.prototype.space = undefined
@@ -0,0 +1,13 @@
let powers = 0
export const boolean = increment()
export const booleanish = increment()
export const overloadedBoolean = increment()
export const number = increment()
export const spaceSeparated = increment()
export const commaSeparated = increment()
export const commaOrSpaceSeparated = increment()
function increment() {
return 2 ** ++powers
}
@@ -0,0 +1,17 @@
import {create} from './util/create.js'
export const xlink = create({
properties: {
xLinkActuate: null,
xLinkArcRole: null,
xLinkHref: null,
xLinkRole: null,
xLinkShow: null,
xLinkTitle: null,
xLinkType: null
},
space: 'xlink',
transform(_, property) {
return 'xlink:' + property.slice(5).toLowerCase()
}
})
@@ -0,0 +1,9 @@
import {create} from './util/create.js'
export const xml = create({
properties: {xmlBase: null, xmlLang: null, xmlSpace: null},
space: 'xml',
transform(_, property) {
return 'xml:' + property.slice(3).toLowerCase()
}
})
@@ -0,0 +1,9 @@
import {create} from './util/create.js'
import {caseInsensitiveTransform} from './util/case-insensitive-transform.js'
export const xmlns = create({
attributes: {xmlnsxlink: 'xmlns:xlink'},
properties: {xmlnsXLink: null, xmlns: null},
space: 'xmlns',
transform: caseInsensitiveTransform
})
@@ -0,0 +1,95 @@
/**
* @typedef {import('unist').Node} Node
* @typedef {import('unist').Point} Point
* @typedef {import('unist').Position} Position
*/
/**
* @typedef NodeLike
* @property {string} type
* @property {PositionLike | null | undefined} [position]
*
* @typedef PositionLike
* @property {PointLike | null | undefined} [start]
* @property {PointLike | null | undefined} [end]
*
* @typedef PointLike
* @property {number | null | undefined} [line]
* @property {number | null | undefined} [column]
* @property {number | null | undefined} [offset]
*/
/**
* Get the ending point of `node`.
*
* @param node
* Node.
* @returns
* Point.
*/
export const pointEnd = point('end')
/**
* Get the starting point of `node`.
*
* @param node
* Node.
* @returns
* Point.
*/
export const pointStart = point('start')
/**
* Get the positional info of `node`.
*
* @param {'end' | 'start'} type
* Side.
* @returns
* Getter.
*/
function point(type) {
return point
/**
* Get the point info of `node` at a bound side.
*
* @param {Node | NodeLike | null | undefined} [node]
* @returns {Point | undefined}
*/
function point(node) {
const point = (node && node.position && node.position[type]) || {}
if (
typeof point.line === 'number' &&
point.line > 0 &&
typeof point.column === 'number' &&
point.column > 0
) {
return {
line: point.line,
column: point.column,
offset:
typeof point.offset === 'number' && point.offset > -1
? point.offset
: undefined
}
}
}
}
/**
* Get the positional info of `node`.
*
* @param {Node | NodeLike | null | undefined} [node]
* Node.
* @returns {Position | undefined}
* Position.
*/
export function position(node) {
const start = pointStart(node)
const end = pointEnd(node)
if (start && end) {
return {start, end}
}
}
@@ -0,0 +1,84 @@
/**
* @typedef {import('unist').Node} Node
* @typedef {import('unist').Point} Point
* @typedef {import('unist').Position} Position
*/
/**
* @typedef NodeLike
* @property {string} type
* @property {PositionLike | null | undefined} [position]
*
* @typedef PointLike
* @property {number | null | undefined} [line]
* @property {number | null | undefined} [column]
* @property {number | null | undefined} [offset]
*
* @typedef PositionLike
* @property {PointLike | null | undefined} [start]
* @property {PointLike | null | undefined} [end]
*/
/**
* Serialize the positional info of a point, position (start and end points),
* or node.
*
* @param {Node | NodeLike | Point | PointLike | Position | PositionLike | null | undefined} [value]
* Node, position, or point.
* @returns {string}
* Pretty printed positional info of a node (`string`).
*
* In the format of a range `ls:cs-le:ce` (when given `node` or `position`)
* or a point `l:c` (when given `point`), where `l` stands for line, `c` for
* column, `s` for `start`, and `e` for end.
* An empty string (`''`) is returned if the given value is neither `node`,
* `position`, nor `point`.
*/
export function stringifyPosition(value) {
// Nothing.
if (!value || typeof value !== 'object') {
return ''
}
// Node.
if ('position' in value || 'type' in value) {
return position(value.position)
}
// Position.
if ('start' in value || 'end' in value) {
return position(value)
}
// Point.
if ('line' in value || 'column' in value) {
return point(value)
}
// ?
return ''
}
/**
* @param {Point | PointLike | null | undefined} point
* @returns {string}
*/
function point(point) {
return index(point && point.line) + ':' + index(point && point.column)
}
/**
* @param {Position | PositionLike | null | undefined} pos
* @returns {string}
*/
function position(pos) {
return point(pos && pos.start) + '-' + point(pos && pos.end)
}
/**
* @param {number | null | undefined} value
* @returns {number}
*/
function index(value) {
return value && typeof value === 'number' ? value : 1
}
@@ -0,0 +1,314 @@
/**
* @import {Node, Point, Position} from 'unist'
*/
/**
* @typedef {object & {type: string, position?: Position | undefined}} NodeLike
*
* @typedef Options
* Configuration.
* @property {Array<Node> | null | undefined} [ancestors]
* Stack of (inclusive) ancestor nodes surrounding the message (optional).
* @property {Error | null | undefined} [cause]
* Original error cause of the message (optional).
* @property {Point | Position | null | undefined} [place]
* Place of message (optional).
* @property {string | null | undefined} [ruleId]
* Category of message (optional, example: `'my-rule'`).
* @property {string | null | undefined} [source]
* Namespace of who sent the message (optional, example: `'my-package'`).
*/
import {stringifyPosition} from 'unist-util-stringify-position'
/**
* Message.
*/
export class VFileMessage extends Error {
/**
* Create a message for `reason`.
*
* > 🪦 **Note**: also has obsolete signatures.
*
* @overload
* @param {string} reason
* @param {Options | null | undefined} [options]
* @returns
*
* @overload
* @param {string} reason
* @param {Node | NodeLike | null | undefined} parent
* @param {string | null | undefined} [origin]
* @returns
*
* @overload
* @param {string} reason
* @param {Point | Position | null | undefined} place
* @param {string | null | undefined} [origin]
* @returns
*
* @overload
* @param {string} reason
* @param {string | null | undefined} [origin]
* @returns
*
* @overload
* @param {Error | VFileMessage} cause
* @param {Node | NodeLike | null | undefined} parent
* @param {string | null | undefined} [origin]
* @returns
*
* @overload
* @param {Error | VFileMessage} cause
* @param {Point | Position | null | undefined} place
* @param {string | null | undefined} [origin]
* @returns
*
* @overload
* @param {Error | VFileMessage} cause
* @param {string | null | undefined} [origin]
* @returns
*
* @param {Error | VFileMessage | string} causeOrReason
* Reason for message, should use markdown.
* @param {Node | NodeLike | Options | Point | Position | string | null | undefined} [optionsOrParentOrPlace]
* Configuration (optional).
* @param {string | null | undefined} [origin]
* Place in code where the message originates (example:
* `'my-package:my-rule'` or `'my-rule'`).
* @returns
* Instance of `VFileMessage`.
*/
// eslint-disable-next-line complexity
constructor(causeOrReason, optionsOrParentOrPlace, origin) {
super()
if (typeof optionsOrParentOrPlace === 'string') {
origin = optionsOrParentOrPlace
optionsOrParentOrPlace = undefined
}
/** @type {string} */
let reason = ''
/** @type {Options} */
let options = {}
let legacyCause = false
if (optionsOrParentOrPlace) {
// Point.
if (
'line' in optionsOrParentOrPlace &&
'column' in optionsOrParentOrPlace
) {
options = {place: optionsOrParentOrPlace}
}
// Position.
else if (
'start' in optionsOrParentOrPlace &&
'end' in optionsOrParentOrPlace
) {
options = {place: optionsOrParentOrPlace}
}
// Node.
else if ('type' in optionsOrParentOrPlace) {
options = {
ancestors: [optionsOrParentOrPlace],
place: optionsOrParentOrPlace.position
}
}
// Options.
else {
options = {...optionsOrParentOrPlace}
}
}
if (typeof causeOrReason === 'string') {
reason = causeOrReason
}
// Error.
else if (!options.cause && causeOrReason) {
legacyCause = true
reason = causeOrReason.message
options.cause = causeOrReason
}
if (!options.ruleId && !options.source && typeof origin === 'string') {
const index = origin.indexOf(':')
if (index === -1) {
options.ruleId = origin
} else {
options.source = origin.slice(0, index)
options.ruleId = origin.slice(index + 1)
}
}
if (!options.place && options.ancestors && options.ancestors) {
const parent = options.ancestors[options.ancestors.length - 1]
if (parent) {
options.place = parent.position
}
}
const start =
options.place && 'start' in options.place
? options.place.start
: options.place
/**
* Stack of ancestor nodes surrounding the message.
*
* @type {Array<Node> | undefined}
*/
this.ancestors = options.ancestors || undefined
/**
* Original error cause of the message.
*
* @type {Error | undefined}
*/
this.cause = options.cause || undefined
/**
* Starting column of message.
*
* @type {number | undefined}
*/
this.column = start ? start.column : undefined
/**
* State of problem.
*
* * `true` — error, file not usable
* * `false` — warning, change may be needed
* * `undefined` — change likely not needed
*
* @type {boolean | null | undefined}
*/
this.fatal = undefined
/**
* Path of a file (used throughout the `VFile` ecosystem).
*
* @type {string | undefined}
*/
this.file = ''
// Field from `Error`.
/**
* Reason for message.
*
* @type {string}
*/
this.message = reason
/**
* Starting line of error.
*
* @type {number | undefined}
*/
this.line = start ? start.line : undefined
// Field from `Error`.
/**
* Serialized positional info of message.
*
* On normal errors, this would be something like `ParseError`, buit in
* `VFile` messages we use this space to show where an error happened.
*/
this.name = stringifyPosition(options.place) || '1:1'
/**
* Place of message.
*
* @type {Point | Position | undefined}
*/
this.place = options.place || undefined
/**
* Reason for message, should use markdown.
*
* @type {string}
*/
this.reason = this.message
/**
* Category of message (example: `'my-rule'`).
*
* @type {string | undefined}
*/
this.ruleId = options.ruleId || undefined
/**
* Namespace of message (example: `'my-package'`).
*
* @type {string | undefined}
*/
this.source = options.source || undefined
// Field from `Error`.
/**
* Stack of message.
*
* This is used by normal errors to show where something happened in
* programming code, irrelevant for `VFile` messages,
*
* @type {string}
*/
this.stack =
legacyCause && options.cause && typeof options.cause.stack === 'string'
? options.cause.stack
: ''
// The following fields are “well known”.
// Not standard.
// Feel free to add other non-standard fields to your messages.
/**
* Specify the source value thats being reported, which is deemed
* incorrect.
*
* @type {string | undefined}
*/
this.actual = undefined
/**
* Suggest acceptable values that can be used instead of `actual`.
*
* @type {Array<string> | undefined}
*/
this.expected = undefined
/**
* Long form description of the message (you should use markdown).
*
* @type {string | undefined}
*/
this.note = undefined
/**
* Link to docs for the message.
*
* > 👉 **Note**: this must be an absolute URL that can be passed as `x`
* > to `new URL(x)`.
*
* @type {string | undefined}
*/
this.url = undefined
}
}
VFileMessage.prototype.file = ''
VFileMessage.prototype.name = ''
VFileMessage.prototype.reason = ''
VFileMessage.prototype.message = ''
VFileMessage.prototype.stack = ''
VFileMessage.prototype.column = undefined
VFileMessage.prototype.line = undefined
VFileMessage.prototype.ancestors = undefined
VFileMessage.prototype.cause = undefined
VFileMessage.prototype.fatal = undefined
VFileMessage.prototype.place = undefined
VFileMessage.prototype.ruleId = undefined
VFileMessage.prototype.source = undefined