Skip to content
This repository was archived by the owner on Oct 1, 2024. It is now read-only.

Commit d259593

Browse files
committed
Add JSDoc based types
1 parent 7e34f23 commit d259593

File tree

7 files changed

+223
-72
lines changed

7 files changed

+223
-72
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
coverage/
22
node_modules/
33
.DS_Store
4+
lib/**/*.d.ts
5+
test/**/*.d.ts
46
*.log
57
yarn.lock

index.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type {Root} from 'mdast'
2+
import type {ReactElement} from 'react'
3+
import type {Plugin} from 'unified'
4+
import type {Options} from './lib/index.js'
5+
6+
/**
7+
* Plugin to compile to React
8+
*
9+
* @param options
10+
* Configuration.
11+
*/
12+
// Note: defining all react nodes as result value seems to trip TS up.
13+
const remarkReact: Plugin<[Options], Root, ReactElement<unknown>>
14+
export default remarkReact
15+
export type {Options}

index.js

+2-60
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,3 @@
1-
import {toHast} from 'mdast-util-to-hast'
2-
import {sanitize} from 'hast-util-sanitize'
3-
import {toH} from 'hast-to-hyperscript'
4-
import tableCellStyle from '@mapbox/hast-util-table-cell-style'
1+
import remarkReact from './lib/index.js'
52

6-
const own = {}.hasOwnProperty
7-
8-
const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr'])
9-
10-
export default function remarkReact(options) {
11-
const settings = options || {}
12-
const createElement = settings.createElement
13-
const Fragment = settings.Fragment || settings.fragment
14-
const clean = settings.sanitize !== false
15-
const scheme =
16-
clean && typeof settings.sanitize !== 'boolean' ? settings.sanitize : null
17-
const toHastOptions = settings.toHast || {}
18-
const components = settings.remarkReactComponents || {}
19-
20-
this.Compiler = compile
21-
22-
// Wrapper around `createElement` to pass components in.
23-
function h(name, props, children) {
24-
// Currently, React issues a warning for *any* white space in tables.
25-
// So we remove the pretty lines for now.
26-
// See: <https://github.com/facebook/react/pull/7081>.
27-
// See: <https://github.com/facebook/react/pull/7515>.
28-
// See: <https://github.com/remarkjs/remark-react/issues/64>.
29-
/* istanbul ignore next - still works but need to publish `remark-gfm`
30-
* first. */
31-
if (children && tableElements.has(name)) {
32-
children = children.filter((child) => {
33-
return child !== '\n'
34-
})
35-
}
36-
37-
return createElement(
38-
own.call(components, name) ? components[name] : name,
39-
props,
40-
children
41-
)
42-
}
43-
44-
// Compile mdast to React.
45-
function compile(node) {
46-
let tree = toHast(node, toHastOptions)
47-
48-
if (clean) {
49-
tree = sanitize(tree, scheme)
50-
}
51-
52-
let root = toH(h, tableCellStyle(tree), settings.prefix)
53-
54-
// If this compiled to a `<div>`, but fragment are possible, use those.
55-
if (root.type === 'div' && Fragment) {
56-
root = createElement(Fragment, {}, root.props.children)
57-
}
58-
59-
return root
60-
}
61-
}
3+
export default remarkReact

lib/index.js

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* @typedef {import('mdast').Root} Root
3+
* @typedef {import('hast-util-sanitize').Schema} Schema
4+
* @typedef {import('mdast-util-to-hast').Options} ToHastOptions
5+
* @typedef {import('react').ReactNode} ReactNode
6+
* @typedef {import('react').ReactElement<unknown>} ReactElement
7+
*
8+
* @callback CreateElementLike
9+
* @param {any} name
10+
* @param {any} props
11+
* @param {...ReactNode} children
12+
* @returns {ReactNode}
13+
*
14+
* @typedef Options
15+
* @property {CreateElementLike} createElement
16+
* How to create elements or components.
17+
* You should typically pass `React.createElement`.
18+
* @property {((props: any) => ReactNode)|undefined} [Fragment]
19+
* Create fragments instead of an outer `<div>` if available.
20+
* You should typically pass `React.Fragment`.
21+
* @property {string|undefined} [prefix='h-']
22+
* React key prefix
23+
* @property {boolean|Schema} [sanitize]
24+
* Options for `hast-util-sanitize`.
25+
* @property {ToHastOptions} [toHast={}]
26+
* Options for `mdast-util-to-hast`.
27+
* @property {Partial<{[TagName in keyof JSX.IntrinsicElements]: string|((props: JSX.IntrinsicElements[TagName]) => ReactNode)}>} [remarkReactComponents]
28+
* Override default elements (such as `<a>`, `<p>`, etcetera) by passing an
29+
* object mapping tag names to components.
30+
*/
31+
32+
import {toHast} from 'mdast-util-to-hast'
33+
import {sanitize} from 'hast-util-sanitize'
34+
import {toH} from 'hast-to-hyperscript'
35+
// @ts-expect-error: untyped.
36+
import tableCellStyle from '@mapbox/hast-util-table-cell-style'
37+
38+
const own = {}.hasOwnProperty
39+
const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr'])
40+
41+
/**
42+
* Plugin to transform markdown to React.
43+
*
44+
* @type {import('unified').Plugin<[Options], Root, ReactElement>}
45+
*/
46+
export default function remarkReact(options) {
47+
if (!options || !options.createElement) {
48+
throw new Error('Missing `createElement` in `options`')
49+
}
50+
51+
const createElement = options.createElement
52+
/** @type {Options['Fragment']} */
53+
// @ts-expect-error: to do: deprecate `fragment`.
54+
const Fragment = options.Fragment || options.fragment
55+
const clean = options.sanitize !== false
56+
const scheme =
57+
clean && typeof options.sanitize !== 'boolean' ? options.sanitize : null
58+
const toHastOptions = options.toHast || {}
59+
const components = options.remarkReactComponents || {}
60+
61+
Object.assign(this, {Compiler: compile})
62+
63+
/**
64+
* @param {keyof JSX.IntrinsicElements} name
65+
* @param {Record<string, unknown>} props
66+
* @param {unknown[]} [children]
67+
* @returns {ReactNode}
68+
*/
69+
function h(name, props, children) {
70+
// Currently, React issues a warning for *any* white space in tables.
71+
// So we remove the pretty lines for now.
72+
// See: <https://github.com/facebook/react/pull/7081>.
73+
// See: <https://github.com/facebook/react/pull/7515>.
74+
// See: <https://github.com/remarkjs/remark-react/issues/64>.
75+
/* istanbul ignore next - still works but need to publish `remark-gfm`
76+
* first. */
77+
if (children && tableElements.has(name)) {
78+
children = children.filter((child) => {
79+
return child !== '\n'
80+
})
81+
}
82+
83+
return createElement(
84+
own.call(components, name) ? components[name] : name,
85+
props,
86+
children
87+
)
88+
}
89+
90+
// Compile mdast to React.
91+
/** @type {import('unified').CompilerFunction<Root, ReactNode>} */
92+
function compile(node) {
93+
let tree = toHast(node, toHastOptions)
94+
95+
if (clean && tree) {
96+
tree = sanitize(tree, scheme || undefined)
97+
}
98+
99+
/** @type {ReactNode} */
100+
// @ts-expect-error: assume `name` is a known element.
101+
let result = toH(h, tableCellStyle(tree), options.prefix)
102+
103+
// If this compiled to a `<div>`, but fragment are possible, use those.
104+
if (
105+
result &&
106+
typeof result === 'object' &&
107+
'type' in result &&
108+
result.type === 'div' &&
109+
'props' in result &&
110+
Fragment
111+
) {
112+
// `children` does exist.
113+
// type-coverage:ignore-next-line
114+
result = createElement(Fragment, {}, result.props.children)
115+
}
116+
117+
return result
118+
}
119+
}

package.json

+24-3
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,24 @@
4040
"sideEffects": false,
4141
"type": "module",
4242
"main": "index.js",
43+
"types": "index.d.ts",
4344
"files": [
45+
"lib/",
46+
"index.d.ts",
4447
"index.js"
4548
],
4649
"dependencies": {
4750
"@mapbox/hast-util-table-cell-style": "^0.2.0",
51+
"@types/mdast": "^3.0.0",
52+
"@types/react": "^17.0.0",
4853
"hast-to-hyperscript": "^10.0.0",
4954
"hast-util-sanitize": "^4.0.0",
50-
"mdast-util-to-hast": "^11.0.0"
55+
"mdast-util-to-hast": "^11.0.0",
56+
"unified": "^10.0.0"
5157
},
5258
"devDependencies": {
59+
"@types/react-dom": "^17.0.0",
60+
"@types/tape": "^4.0.0",
5361
"c8": "^7.0.0",
5462
"is-hidden": "^2.0.0",
5563
"prettier": "^2.0.0",
@@ -60,16 +68,19 @@
6068
"remark-footnotes": "^4.0.0",
6169
"remark-frontmatter": "^4.0.0",
6270
"remark-preset-wooorm": "^8.0.0",
71+
"rimraf": "^3.0.0",
6372
"tape": "^5.0.0",
73+
"type-coverage": "^2.0.0",
74+
"typescript": "^4.0.0",
6475
"vfile": "^5.0.0",
6576
"xo": "^0.39.0"
6677
},
6778
"scripts": {
68-
"sub-install": "cd test/react/v17 && npm install && cd ../..",
79+
"build": "rimraf \"lib/**/*.d.ts\" \"test/**/*.d.ts\" && tsc && type-coverage",
6980
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
7081
"test-api": "node --conditions development test/index.js",
7182
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
72-
"test": "npm run sub-install && npm run format && npm run test-coverage"
83+
"test": "npm run build && npm run format && npm run test-coverage"
7384
},
7485
"prettier": {
7586
"tabWidth": 2,
@@ -86,5 +97,15 @@
8697
"plugins": [
8798
"preset-wooorm"
8899
]
100+
},
101+
"typeCoverage": {
102+
"atLeast": 100,
103+
"detail": true,
104+
"strict": true,
105+
"ignoreCatch": true,
106+
"#": "needed `any`s",
107+
"ignoreFiles": [
108+
"lib/index.d.ts"
109+
]
89110
}
90111
}

test/index.js

+45-9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* @typedef {import('mdast').Image} Image
3+
* @typedef {import('react').ReactNode} ReactNode
4+
* @typedef {import('react').ReactElement<unknown>} ReactElement
5+
*/
6+
17
import path from 'path'
28
import fs from 'fs'
39
import test from 'tape'
@@ -12,9 +18,14 @@ import {renderToStaticMarkup} from 'react-dom/server.js'
1218
import remarkReact from '../index.js'
1319

1420
test('React ' + React.version, (t) => {
15-
t.doesNotThrow(() => {
16-
remark().use(remarkReact).freeze()
17-
}, 'should not throw if not passed options')
21+
t.throws(
22+
() => {
23+
// @ts-expect-error: Options missing.
24+
remark().use(remarkReact).freeze()
25+
},
26+
/Missing `createElement` in `options`/,
27+
'should throw if not passed options'
28+
)
1829

1930
t.test('should use consistent keys on multiple renders', (st) => {
2031
const markdown = '# A **bold** heading'
@@ -23,6 +34,9 @@ test('React ' + React.version, (t) => {
2334

2435
st.end()
2536

37+
/**
38+
* @param {string} text
39+
*/
2640
function reactKeys(text) {
2741
return extractKeys(
2842
remark()
@@ -31,14 +45,29 @@ test('React ' + React.version, (t) => {
3145
)
3246
}
3347

48+
/**
49+
* @param {ReactElement} reactElement
50+
* @returns {Array.<string|number>}
51+
*/
3452
function extractKeys(reactElement) {
53+
/** @type {Array.<string|number>} */
3554
const keys = []
3655

3756
if (reactElement.key !== undefined && reactElement.key !== null) {
3857
keys.push(reactElement.key)
3958
}
4059

41-
if (reactElement.props !== undefined && reactElement.props !== null) {
60+
if (
61+
reactElement.props &&
62+
typeof reactElement.props === 'object' &&
63+
// `children` does exist.
64+
// @ts-expect-error
65+
// type-coverage:ignore-next-line
66+
reactElement.props.children
67+
) {
68+
// `children` does exist.
69+
// @ts-expect-error
70+
// type-coverage:ignore-next-line
4271
React.Children.forEach(reactElement.props.children, (child) => {
4372
keys.push(...extractKeys(child))
4473
})
@@ -97,12 +126,17 @@ test('React ' + React.version, (t) => {
97126
remark()
98127
.use(remarkReact, {
99128
createElement: React.createElement,
100-
toHast: {commonmark: true}
129+
toHast: {
130+
handlers: {
131+
image(_, /** @type {Image} */ node) {
132+
return {type: 'text', value: node.alt || ''}
133+
}
134+
}
135+
}
101136
})
102-
.processSync('[reference]\n\n[reference]: a.com\n[reference]: b.com')
103-
.result
137+
.processSync('![a]()').result
104138
),
105-
'<p><a href="a.com">reference</a></p>',
139+
'<p>a</p>',
106140
'passes toHast options to inner toHast() function'
107141
)
108142

@@ -121,7 +155,9 @@ test('React ' + React.version, (t) => {
121155
let config = {}
122156

123157
try {
124-
config = JSON.parse(fs.readFileSync(path.join(base, 'config.json')))
158+
config = JSON.parse(
159+
String(fs.readFileSync(path.join(base, 'config.json')))
160+
)
125161
} catch {}
126162

127163
config.createElement = React.createElement

tsconfig.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"include": ["lib/**/*.js", "test/**/*.js"],
3+
"compilerOptions": {
4+
"target": "ES2020",
5+
"lib": ["ES2020"],
6+
"module": "ES2020",
7+
"moduleResolution": "node",
8+
"allowJs": true,
9+
"checkJs": true,
10+
"declaration": true,
11+
"emitDeclarationOnly": true,
12+
"allowSyntheticDefaultImports": true,
13+
"skipLibCheck": true,
14+
"strict": true
15+
}
16+
}

0 commit comments

Comments
 (0)