This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
/**
* @param {Context} context
* @param {Element|Root} node
*/
export function childrenToReact(
context: Context,
node: Element | Root
): React.ReactNode[]
/**
* <T>
*/
export type ComponentType<T> = import('react').ComponentType<T>
/**
* <T>
*/
export type ComponentPropsWithoutRef<T extends React.ElementType<any>> =
import('react').ComponentPropsWithoutRef<T>
export type ReactNode = import('react').ReactNode
export type Position = import('unist').Position
export type Element = import('hast').Element
export type ElementContent = import('hast').ElementContent
export type Root = import('hast').Root
export type Text = import('hast').Text
export type Comment = import('hast').Comment
export type Doctype = import('hast').DocType
export type Info = import('property-information').Info
export type Schema = import('property-information').Schema
export type ReactMarkdownProps = import('./complex-types.js').ReactMarkdownProps
export type Raw = {
type: 'raw'
value: string
}
export type Context = {
options: Options
schema: Schema
listDepth: number
}
export type TransformLink = (
href: string,
children: Array<ElementContent>,
title: string | null
) => string
export type TransformImage = (
src: string,
alt: string,
title: string | null
) => string
export type TransformLinkTargetType = import('react').HTMLAttributeAnchorTarget
export type TransformLinkTarget = (
href: string,
children: Array<ElementContent>,
title: string | null
) => TransformLinkTargetType | undefined
/**
* To do: is `data-sourcepos` typeable?
*/
export type ReactMarkdownNames = keyof JSX.IntrinsicElements
export type CodeProps = ComponentPropsWithoutRef<'code'> &
ReactMarkdownProps & {
inline?: boolean
}
export type HeadingProps = ComponentPropsWithoutRef<'h1'> &
ReactMarkdownProps & {
level: number
}
export type LiProps = ComponentPropsWithoutRef<'li'> &
ReactMarkdownProps & {
checked: boolean | null
index: number
ordered: boolean
}
export type OrderedListProps = ComponentPropsWithoutRef<'ol'> &
ReactMarkdownProps & {
depth: number
ordered: true
}
export type TableDataCellProps = ComponentPropsWithoutRef<'td'> &
ReactMarkdownProps & {
style?: Record<string, unknown>
isHeader: false
}
export type TableHeaderCellProps = ComponentPropsWithoutRef<'th'> &
ReactMarkdownProps & {
style?: Record<string, unknown>
isHeader: true
}
export type TableRowProps = ComponentPropsWithoutRef<'tr'> &
ReactMarkdownProps & {
isHeader: boolean
}
export type UnorderedListProps = ComponentPropsWithoutRef<'ul'> &
ReactMarkdownProps & {
depth: number
ordered: false
}
export type CodeComponent = ComponentType<CodeProps>
export type HeadingComponent = ComponentType<HeadingProps>
export type LiComponent = ComponentType<LiProps>
export type OrderedListComponent = ComponentType<OrderedListProps>
export type TableDataCellComponent = ComponentType<TableDataCellProps>
export type TableHeaderCellComponent = ComponentType<TableHeaderCellProps>
export type TableRowComponent = ComponentType<TableRowProps>
export type UnorderedListComponent = ComponentType<UnorderedListProps>
export type SpecialComponents = {
code: CodeComponent | ReactMarkdownNames
h1: HeadingComponent | ReactMarkdownNames
h2: HeadingComponent | ReactMarkdownNames
h3: HeadingComponent | ReactMarkdownNames
h4: HeadingComponent | ReactMarkdownNames
h5: HeadingComponent | ReactMarkdownNames
h6: HeadingComponent | ReactMarkdownNames
li: LiComponent | ReactMarkdownNames
ol: OrderedListComponent | ReactMarkdownNames
td: TableDataCellComponent | ReactMarkdownNames
th: TableHeaderCellComponent | ReactMarkdownNames
tr: TableRowComponent | ReactMarkdownNames
ul: UnorderedListComponent | ReactMarkdownNames
}
export type Components = Partial<
Omit<import('./complex-types.js').NormalComponents, keyof SpecialComponents> &
SpecialComponents
>
export type Options = {
sourcePos?: boolean
rawSourcePos?: boolean
skipHtml?: boolean
includeElementIndex?: boolean
transformLinkUri?: null | false | TransformLink
transformImageUri?: TransformImage
linkTarget?: TransformLinkTargetType | TransformLinkTarget
components?: Components
}
import React from 'react'
import style from 'style-to-object'

View File

@@ -0,0 +1,451 @@
/**
* @template T
* @typedef {import('react').ComponentType<T>} ComponentType<T>
*/
/**
* @template {import('react').ElementType} T
* @typedef {import('react').ComponentPropsWithoutRef<T>} ComponentPropsWithoutRef<T>
*/
/**
* @typedef {import('react').ReactNode} ReactNode
* @typedef {import('unist').Position} Position
* @typedef {import('hast').Element} Element
* @typedef {import('hast').ElementContent} ElementContent
* @typedef {import('hast').Root} Root
* @typedef {import('hast').Text} Text
* @typedef {import('hast').Comment} Comment
* @typedef {import('hast').DocType} Doctype
* @typedef {import('property-information').Info} Info
* @typedef {import('property-information').Schema} Schema
* @typedef {import('./complex-types.js').ReactMarkdownProps} ReactMarkdownProps
*
* @typedef Raw
* @property {'raw'} type
* @property {string} value
*
* @typedef Context
* @property {Options} options
* @property {Schema} schema
* @property {number} listDepth
*
* @callback TransformLink
* @param {string} href
* @param {Array<ElementContent>} children
* @param {string?} title
* @returns {string}
*
* @callback TransformImage
* @param {string} src
* @param {string} alt
* @param {string?} title
* @returns {string}
*
* @typedef {import('react').HTMLAttributeAnchorTarget} TransformLinkTargetType
*
* @callback TransformLinkTarget
* @param {string} href
* @param {Array<ElementContent>} children
* @param {string?} title
* @returns {TransformLinkTargetType|undefined}
*
* @typedef {keyof JSX.IntrinsicElements} ReactMarkdownNames
*
* To do: is `data-sourcepos` typeable?
*
* @typedef {ComponentPropsWithoutRef<'code'> & ReactMarkdownProps & {inline?: boolean}} CodeProps
* @typedef {ComponentPropsWithoutRef<'h1'> & ReactMarkdownProps & {level: number}} HeadingProps
* @typedef {ComponentPropsWithoutRef<'li'> & ReactMarkdownProps & {checked: boolean|null, index: number, ordered: boolean}} LiProps
* @typedef {ComponentPropsWithoutRef<'ol'> & ReactMarkdownProps & {depth: number, ordered: true}} OrderedListProps
* @typedef {ComponentPropsWithoutRef<'td'> & ReactMarkdownProps & {style?: Record<string, unknown>, isHeader: false}} TableDataCellProps
* @typedef {ComponentPropsWithoutRef<'th'> & ReactMarkdownProps & {style?: Record<string, unknown>, isHeader: true}} TableHeaderCellProps
* @typedef {ComponentPropsWithoutRef<'tr'> & ReactMarkdownProps & {isHeader: boolean}} TableRowProps
* @typedef {ComponentPropsWithoutRef<'ul'> & ReactMarkdownProps & {depth: number, ordered: false}} UnorderedListProps
*
* @typedef {ComponentType<CodeProps>} CodeComponent
* @typedef {ComponentType<HeadingProps>} HeadingComponent
* @typedef {ComponentType<LiProps>} LiComponent
* @typedef {ComponentType<OrderedListProps>} OrderedListComponent
* @typedef {ComponentType<TableDataCellProps>} TableDataCellComponent
* @typedef {ComponentType<TableHeaderCellProps>} TableHeaderCellComponent
* @typedef {ComponentType<TableRowProps>} TableRowComponent
* @typedef {ComponentType<UnorderedListProps>} UnorderedListComponent
*
* @typedef SpecialComponents
* @property {CodeComponent|ReactMarkdownNames} code
* @property {HeadingComponent|ReactMarkdownNames} h1
* @property {HeadingComponent|ReactMarkdownNames} h2
* @property {HeadingComponent|ReactMarkdownNames} h3
* @property {HeadingComponent|ReactMarkdownNames} h4
* @property {HeadingComponent|ReactMarkdownNames} h5
* @property {HeadingComponent|ReactMarkdownNames} h6
* @property {LiComponent|ReactMarkdownNames} li
* @property {OrderedListComponent|ReactMarkdownNames} ol
* @property {TableDataCellComponent|ReactMarkdownNames} td
* @property {TableHeaderCellComponent|ReactMarkdownNames} th
* @property {TableRowComponent|ReactMarkdownNames} tr
* @property {UnorderedListComponent|ReactMarkdownNames} ul
*
* @typedef {Partial<Omit<import('./complex-types.js').NormalComponents, keyof SpecialComponents> & SpecialComponents>} Components
*
* @typedef Options
* @property {boolean} [sourcePos=false]
* @property {boolean} [rawSourcePos=false]
* @property {boolean} [skipHtml=false]
* @property {boolean} [includeElementIndex=false]
* @property {null|false|TransformLink} [transformLinkUri]
* @property {TransformImage} [transformImageUri]
* @property {TransformLinkTargetType|TransformLinkTarget} [linkTarget]
* @property {Components} [components]
*/
import React from 'react'
import ReactIs from 'react-is'
import {whitespace} from 'hast-util-whitespace'
import {svg, find, hastToReact} from 'property-information'
import {stringify as spaces} from 'space-separated-tokens'
import {stringify as commas} from 'comma-separated-tokens'
import style from 'style-to-object'
import {uriTransformer} from './uri-transformer.js'
const own = {}.hasOwnProperty
// The table-related elements that must not contain whitespace text according
// to React.
const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr'])
/**
* @param {Context} context
* @param {Element|Root} node
*/
export function childrenToReact(context, node) {
/** @type {Array<ReactNode>} */
const children = []
let childIndex = -1
/** @type {Comment|Doctype|Element|Raw|Text} */
let child
while (++childIndex < node.children.length) {
child = node.children[childIndex]
if (child.type === 'element') {
children.push(toReact(context, child, childIndex, node))
} else if (child.type === 'text') {
// Currently, a warning is triggered by react for *any* white space in
// tables.
// So we drop it.
// 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/remarkjs/react-markdown/issues/576>.
if (
node.type !== 'element' ||
!tableElements.has(node.tagName) ||
!whitespace(child)
) {
children.push(child.value)
}
} else if (child.type === 'raw' && !context.options.skipHtml) {
// Default behavior is to show (encoded) HTML.
children.push(child.value)
}
}
return children
}
/**
* @param {Context} context
* @param {Element} node
* @param {number} index
* @param {Element|Root} parent
*/
function toReact(context, node, index, parent) {
const options = context.options
const transform =
options.transformLinkUri === undefined
? uriTransformer
: options.transformLinkUri
const parentSchema = context.schema
/** @type {ReactMarkdownNames} */
// @ts-expect-error assume a known HTML/SVG element.
const name = node.tagName
/** @type {Record<string, unknown>} */
const properties = {}
let schema = parentSchema
/** @type {string} */
let property
if (parentSchema.space === 'html' && name === 'svg') {
schema = svg
context.schema = schema
}
if (node.properties) {
for (property in node.properties) {
if (own.call(node.properties, property)) {
addProperty(properties, property, node.properties[property], context)
}
}
}
if (name === 'ol' || name === 'ul') {
context.listDepth++
}
const children = childrenToReact(context, node)
if (name === 'ol' || name === 'ul') {
context.listDepth--
}
// Restore parent schema.
context.schema = parentSchema
// Nodes created by plugins do not have positional info, in which case we use
// an object that matches the position interface.
const position = node.position || {
start: {line: null, column: null, offset: null},
end: {line: null, column: null, offset: null}
}
const component =
options.components && own.call(options.components, name)
? options.components[name]
: name
const basic = typeof component === 'string' || component === React.Fragment
if (!ReactIs.isValidElementType(component)) {
throw new TypeError(
`Component for name \`${name}\` not defined or is not renderable`
)
}
properties.key = index
if (name === 'a' && options.linkTarget) {
properties.target =
typeof options.linkTarget === 'function'
? options.linkTarget(
String(properties.href || ''),
node.children,
typeof properties.title === 'string' ? properties.title : null
)
: options.linkTarget
}
if (name === 'a' && transform) {
properties.href = transform(
String(properties.href || ''),
node.children,
typeof properties.title === 'string' ? properties.title : null
)
}
if (
!basic &&
name === 'code' &&
parent.type === 'element' &&
parent.tagName !== 'pre'
) {
properties.inline = true
}
if (
!basic &&
(name === 'h1' ||
name === 'h2' ||
name === 'h3' ||
name === 'h4' ||
name === 'h5' ||
name === 'h6')
) {
properties.level = Number.parseInt(name.charAt(1), 10)
}
if (name === 'img' && options.transformImageUri) {
properties.src = options.transformImageUri(
String(properties.src || ''),
String(properties.alt || ''),
typeof properties.title === 'string' ? properties.title : null
)
}
if (!basic && name === 'li' && parent.type === 'element') {
const input = getInputElement(node)
properties.checked =
input && input.properties ? Boolean(input.properties.checked) : null
properties.index = getElementsBeforeCount(parent, node)
properties.ordered = parent.tagName === 'ol'
}
if (!basic && (name === 'ol' || name === 'ul')) {
properties.ordered = name === 'ol'
properties.depth = context.listDepth
}
if (name === 'td' || name === 'th') {
if (properties.align) {
if (!properties.style) properties.style = {}
// @ts-expect-error assume `style` is an object
properties.style.textAlign = properties.align
delete properties.align
}
if (!basic) {
properties.isHeader = name === 'th'
}
}
if (!basic && name === 'tr' && parent.type === 'element') {
properties.isHeader = Boolean(parent.tagName === 'thead')
}
// If `sourcePos` is given, pass source information (line/column info from markdown source).
if (options.sourcePos) {
properties['data-sourcepos'] = flattenPosition(position)
}
if (!basic && options.rawSourcePos) {
properties.sourcePosition = node.position
}
// If `includeElementIndex` is given, pass node index info to components.
if (!basic && options.includeElementIndex) {
properties.index = getElementsBeforeCount(parent, node)
properties.siblingCount = getElementsBeforeCount(parent)
}
if (!basic) {
properties.node = node
}
// Ensure no React warnings are emitted for void elements w/ children.
return children.length > 0
? React.createElement(component, properties, children)
: React.createElement(component, properties)
}
/**
* @param {Element|Root} node
* @returns {Element?}
*/
function getInputElement(node) {
let index = -1
while (++index < node.children.length) {
const child = node.children[index]
if (child.type === 'element' && child.tagName === 'input') {
return child
}
}
return null
}
/**
* @param {Element|Root} parent
* @param {Element} [node]
* @returns {number}
*/
function getElementsBeforeCount(parent, node) {
let index = -1
let count = 0
while (++index < parent.children.length) {
if (parent.children[index] === node) break
if (parent.children[index].type === 'element') count++
}
return count
}
/**
* @param {Record<string, unknown>} props
* @param {string} prop
* @param {unknown} value
* @param {Context} ctx
*/
function addProperty(props, prop, value, ctx) {
const info = find(ctx.schema, prop)
let result = value
// Ignore nullish and `NaN` values.
// eslint-disable-next-line no-self-compare
if (result === null || result === undefined || result !== result) {
return
}
// Accept `array`.
// Most props are space-separated.
if (Array.isArray(result)) {
result = info.commaSeparated ? commas(result) : spaces(result)
}
if (info.property === 'style' && typeof result === 'string') {
result = parseStyle(result)
}
if (info.space && info.property) {
props[
own.call(hastToReact, info.property)
? hastToReact[info.property]
: info.property
] = result
} else if (info.attribute) {
props[info.attribute] = result
}
}
/**
* @param {string} value
* @returns {Record<string, string>}
*/
function parseStyle(value) {
/** @type {Record<string, string>} */
const result = {}
try {
style(value, iterator)
} catch {
// Silent.
}
return result
/**
* @param {string} name
* @param {string} v
*/
function iterator(name, v) {
const k = name.slice(0, 4) === '-ms-' ? `ms-${name.slice(4)}` : name
result[k.replace(/-([a-z])/g, styleReplacer)] = v
}
}
/**
* @param {unknown} _
* @param {string} $1
*/
function styleReplacer(_, $1) {
return $1.toUpperCase()
}
/**
* @param {Position|{start: {line: null, column: null, offset: null}, end: {line: null, column: null, offset: null}}} pos
* @returns {string}
*/
function flattenPosition(pos) {
return [
pos.start.line,
':',
pos.start.column,
'-',
pos.end.line,
':',
pos.end.column
]
.map(String)
.join('')
}

View File

@@ -0,0 +1,24 @@
import type {ReactNode, ComponentType, ComponentPropsWithoutRef} from 'react'
import type {Position} from 'unist'
import type {Element} from 'hast'
export type ReactMarkdownProps = {
node: Element
children: ReactNode[]
/**
* Passed when `options.rawSourcePos` is given
*/
sourcePosition?: Position
/**
* Passed when `options.includeElementIndex` is given
*/
index?: number
/**
* Passed when `options.includeElementIndex` is given
*/
siblingCount?: number
}
export type NormalComponents = {
[TagName in keyof JSX.IntrinsicElements]:
| keyof JSX.IntrinsicElements
| ComponentType<ComponentPropsWithoutRef<TagName> & ReactMarkdownProps>
}

View File

@@ -0,0 +1,28 @@
import type {ReactNode, ComponentType, ComponentPropsWithoutRef} from 'react'
import type {Position} from 'unist'
import type {Element} from 'hast'
/* File for types which are not handled correctly in JSDoc mode */
export type ReactMarkdownProps = {
node: Element
children: ReactNode[]
/**
* Passed when `options.rawSourcePos` is given
*/
sourcePosition?: Position
/**
* Passed when `options.includeElementIndex` is given
*/
index?: number
/**
* Passed when `options.includeElementIndex` is given
*/
siblingCount?: number
}
export type NormalComponents = {
[TagName in keyof JSX.IntrinsicElements]:
| keyof JSX.IntrinsicElements
| ComponentType<ComponentPropsWithoutRef<TagName> & ReactMarkdownProps>
}

View File

@@ -0,0 +1,60 @@
/**
* React component to render markdown.
*
* @param {ReactMarkdownOptions} options
* @returns {ReactElement}
*/
export function ReactMarkdown(options: ReactMarkdownOptions): ReactElement
export namespace ReactMarkdown {
namespace propTypes {
const children: PropTypes.Requireable<string>
const className: PropTypes.Requireable<string>
const allowElement: PropTypes.Requireable<(...args: any[]) => any>
const allowedElements: PropTypes.Requireable<(string | null | undefined)[]>
const disallowedElements: PropTypes.Requireable<
(string | null | undefined)[]
>
const unwrapDisallowed: PropTypes.Requireable<boolean>
const remarkPlugins: PropTypes.Requireable<(object | null | undefined)[]>
const rehypePlugins: PropTypes.Requireable<(object | null | undefined)[]>
const sourcePos: PropTypes.Requireable<boolean>
const rawSourcePos: PropTypes.Requireable<boolean>
const skipHtml: PropTypes.Requireable<boolean>
const includeElementIndex: PropTypes.Requireable<boolean>
const transformLinkUri: PropTypes.Requireable<
NonNullable<boolean | ((...args: any[]) => any) | null | undefined>
>
const linkTarget: PropTypes.Requireable<
NonNullable<string | ((...args: any[]) => any) | null | undefined>
>
const transformImageUri: PropTypes.Requireable<(...args: any[]) => any>
const components: PropTypes.Requireable<object>
}
}
export type ReactNode = import('react').ReactNode
export type ReactElement = import('react').ReactElement<{}>
export type PluggableList = import('unified').PluggableList
export type Root = import('hast').Root
export type FilterOptions = import('./rehype-filter.js').Options
export type TransformOptions = import('./ast-to-react.js').Options
export type CoreOptions = {
children: string
}
export type PluginOptions = {
remarkPlugins?: import('unified').PluggableList
rehypePlugins?: import('unified').PluggableList
remarkRehypeOptions?: import('remark-rehype').Options | undefined
}
export type LayoutOptions = {
className?: string
}
export type ReactMarkdownOptions = CoreOptions &
PluginOptions &
LayoutOptions &
FilterOptions &
TransformOptions
export type Deprecation = {
id: string
to?: string
}
import PropTypes from 'prop-types'

View File

@@ -0,0 +1,184 @@
/**
* @typedef {import('react').ReactNode} ReactNode
* @typedef {import('react').ReactElement<{}>} ReactElement
* @typedef {import('unified').PluggableList} PluggableList
* @typedef {import('hast').Root} Root
* @typedef {import('./rehype-filter.js').Options} FilterOptions
* @typedef {import('./ast-to-react.js').Options} TransformOptions
*
* @typedef CoreOptions
* @property {string} children
*
* @typedef PluginOptions
* @property {PluggableList} [remarkPlugins=[]]
* @property {PluggableList} [rehypePlugins=[]]
* @property {import('remark-rehype').Options | undefined} [remarkRehypeOptions={}]
*
* @typedef LayoutOptions
* @property {string} [className]
*
* @typedef {CoreOptions & PluginOptions & LayoutOptions & FilterOptions & TransformOptions} ReactMarkdownOptions
*
* @typedef Deprecation
* @property {string} id
* @property {string} [to]
*/
import React from 'react'
import {VFile} from 'vfile'
import {unified} from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import PropTypes from 'prop-types'
import {html} from 'property-information'
import rehypeFilter from './rehype-filter.js'
import {childrenToReact} from './ast-to-react.js'
const own = {}.hasOwnProperty
const changelog =
'https://github.com/remarkjs/react-markdown/blob/main/changelog.md'
/** @type {Record<string, Deprecation>} */
const deprecated = {
plugins: {to: 'remarkPlugins', id: 'change-plugins-to-remarkplugins'},
renderers: {to: 'components', id: 'change-renderers-to-components'},
astPlugins: {id: 'remove-buggy-html-in-markdown-parser'},
allowDangerousHtml: {id: 'remove-buggy-html-in-markdown-parser'},
escapeHtml: {id: 'remove-buggy-html-in-markdown-parser'},
source: {to: 'children', id: 'change-source-to-children'},
allowNode: {
to: 'allowElement',
id: 'replace-allownode-allowedtypes-and-disallowedtypes'
},
allowedTypes: {
to: 'allowedElements',
id: 'replace-allownode-allowedtypes-and-disallowedtypes'
},
disallowedTypes: {
to: 'disallowedElements',
id: 'replace-allownode-allowedtypes-and-disallowedtypes'
},
includeNodeIndex: {
to: 'includeElementIndex',
id: 'change-includenodeindex-to-includeelementindex'
}
}
/**
* React component to render markdown.
*
* @param {ReactMarkdownOptions} options
* @returns {ReactElement}
*/
export function ReactMarkdown(options) {
for (const key in deprecated) {
if (own.call(deprecated, key) && own.call(options, key)) {
const deprecation = deprecated[key]
console.warn(
`[react-markdown] Warning: please ${
deprecation.to ? `use \`${deprecation.to}\` instead of` : 'remove'
} \`${key}\` (see <${changelog}#${deprecation.id}> for more info)`
)
delete deprecated[key]
}
}
const processor = unified()
.use(remarkParse)
.use(options.remarkPlugins || [])
.use(remarkRehype, {
...options.remarkRehypeOptions,
allowDangerousHtml: true
})
.use(options.rehypePlugins || [])
.use(rehypeFilter, options)
const file = new VFile()
if (typeof options.children === 'string') {
file.value = options.children
} else if (options.children !== undefined && options.children !== null) {
console.warn(
`[react-markdown] Warning: please pass a string as \`children\` (not: \`${options.children}\`)`
)
}
const hastNode = processor.runSync(processor.parse(file), file)
if (hastNode.type !== 'root') {
throw new TypeError('Expected a `root` node')
}
/** @type {ReactElement} */
let result = React.createElement(
React.Fragment,
{},
childrenToReact({options, schema: html, listDepth: 0}, hastNode)
)
if (options.className) {
result = React.createElement('div', {className: options.className}, result)
}
return result
}
ReactMarkdown.propTypes = {
// Core options:
children: PropTypes.string,
// Layout options:
className: PropTypes.string,
// Filter options:
allowElement: PropTypes.func,
allowedElements: PropTypes.arrayOf(PropTypes.string),
disallowedElements: PropTypes.arrayOf(PropTypes.string),
unwrapDisallowed: PropTypes.bool,
// Plugin options:
remarkPlugins: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.object,
PropTypes.func,
PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string,
PropTypes.object,
PropTypes.func,
PropTypes.arrayOf(
// prettier-ignore
// type-coverage:ignore-next-line
PropTypes.any
)
])
)
])
),
rehypePlugins: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.object,
PropTypes.func,
PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string,
PropTypes.object,
PropTypes.func,
PropTypes.arrayOf(
// prettier-ignore
// type-coverage:ignore-next-line
PropTypes.any
)
])
)
])
),
// Transform options:
sourcePos: PropTypes.bool,
rawSourcePos: PropTypes.bool,
skipHtml: PropTypes.bool,
includeElementIndex: PropTypes.bool,
transformLinkUri: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
linkTarget: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
transformImageUri: PropTypes.func,
components: PropTypes.object
}

View File

@@ -0,0 +1,20 @@
export default function rehypeFilter(
this: import('unified').Processor<void, import('hast').Root, void, void>,
settings_0: Options
):
| void
| import('unified').Transformer<import('hast').Root, import('hast').Root>
export type Node = import('unist').Node
export type Root = import('hast').Root
export type Element = import('hast').Element
export type AllowElement = (
element: Element,
index: number,
parent: Element | Root
) => boolean | undefined
export type Options = {
allowedElements?: Array<string>
disallowedElements?: Array<string>
allowElement?: AllowElement
unwrapDisallowed?: boolean
}

View File

@@ -0,0 +1,66 @@
import {visit} from 'unist-util-visit'
/**
* @typedef {import('unist').Node} Node
* @typedef {import('hast').Root} Root
* @typedef {import('hast').Element} Element
*
* @callback AllowElement
* @param {Element} element
* @param {number} index
* @param {Element|Root} parent
* @returns {boolean|undefined}
*
* @typedef Options
* @property {Array<string>} [allowedElements]
* @property {Array<string>} [disallowedElements=[]]
* @property {AllowElement} [allowElement]
* @property {boolean} [unwrapDisallowed=false]
*/
/**
* @type {import('unified').Plugin<[Options], Root>}
*/
export default function rehypeFilter(options) {
if (options.allowedElements && options.disallowedElements) {
throw new TypeError(
'Only one of `allowedElements` and `disallowedElements` should be defined'
)
}
if (
options.allowedElements ||
options.disallowedElements ||
options.allowElement
) {
return (tree) => {
visit(tree, 'element', (node, index, parent_) => {
const parent = /** @type {Element|Root} */ (parent_)
/** @type {boolean|undefined} */
let remove
if (options.allowedElements) {
remove = !options.allowedElements.includes(node.tagName)
} else if (options.disallowedElements) {
remove = options.disallowedElements.includes(node.tagName)
}
if (!remove && options.allowElement && typeof index === 'number') {
remove = !options.allowElement(node, index, parent)
}
if (remove && typeof index === 'number') {
if (options.unwrapDisallowed && node.children) {
parent.children.splice(index, 1, ...node.children)
} else {
parent.children.splice(index, 1)
}
return index
}
return undefined
})
}
}
}

View File

@@ -0,0 +1,5 @@
/**
* @param {string} uri
* @returns {string}
*/
export function uriTransformer(uri: string): string

View File

@@ -0,0 +1,45 @@
const protocols = ['http', 'https', 'mailto', 'tel']
/**
* @param {string} uri
* @returns {string}
*/
export function uriTransformer(uri) {
const url = (uri || '').trim()
const first = url.charAt(0)
if (first === '#' || first === '/') {
return url
}
const colon = url.indexOf(':')
if (colon === -1) {
return url
}
let index = -1
while (++index < protocols.length) {
const protocol = protocols[index]
if (
colon === protocol.length &&
url.slice(0, protocol.length).toLowerCase() === protocol
) {
return url
}
}
index = url.indexOf('?')
if (index !== -1 && colon > index) {
return url
}
index = url.indexOf('#')
if (index !== -1 && colon > index) {
return url
}
// eslint-disable-next-line no-script-url
return 'javascript:void(0)'
}