Skip to content

Commit

Permalink
Add JSDoc based types
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Aug 5, 2021
1 parent 31004cb commit d86dd92
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 26 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
coverage/
node_modules/
.DS_Store
*.d.ts
*.log
yarn.lock
93 changes: 73 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,104 @@
/**
* @typedef {import('mdast').Root} MdastRoot
* @typedef {import('hast').Root} HastRoot
* @typedef {import('hast').Element} HastElement
* @typedef {HastRoot|HastRoot['children'][number]} HastNode
* @typedef {import('virtual-dom').VNode} VNode
* @typedef {import('virtual-dom').VChild} VChild
* @typedef {import('virtual-dom').h} H
* @typedef {import('hast-util-sanitize').Schema} Schema
*
* @callback Component
* @param {string} name
* @param {Record<string, unknown>} props
* @param {VNode[]} children
* @returns {VNode|VNode[]}
*
* @typedef Options
* Configuration.
* @property {boolean|Schema|null|undefined} [sanitize]
* How to sanitize the output.
*
* Sanitation is done by `hast-util-sanitize`, except when `false` is
* given.
* If an object is passed in, it’s given as a schema to `sanitize`.
* By default, input is sanitized according to GitHub’s sanitation rules.
*
* Embedded HTML is **always** stripped.
* @property {string|null|undefined} [prefix='h-']
* Optimization hint.
* @property {H|null|undefined} [h]
* Hyperscript to use.
* @property {Record<string, Component>} [components={}]
* Map of tag names to custom components.
* That component is invoked with `tagName`, `props`, and `children`.
* It can return any VDOM compatible value (such as `VNode`, `VText`,
* `Widget`).
*/

import {toHast} from 'mdast-util-to-hast'
import {sanitize} from 'hast-util-sanitize'
import {toH} from 'hast-to-hyperscript'
import hyperscript from 'virtual-dom/h.js'
import {h as defaultH} from 'virtual-dom'

const own = {}.hasOwnProperty

// Attach a VDOM compiler.
export default function remarkVdom(options) {
const settings = options || {}
const info = settings.sanitize
/**
* Plugin to compile Markdown to Virtual DOM.
*
* @type {import('unified').Plugin<[Options?]|void[], MdastRoot, VNode>}
*/
export default function remarkVdom(options = {}) {
const info = options.sanitize
const clean = info !== false
const schema = info && typeof info === 'object' ? info : null
const components = settings.components || {}
const h = settings.h || hyperscript
const components = options.components || {}
const h = options.h || defaultH

this.Compiler = compiler
Object.assign(this, {Compiler: compiler})

// Compile mdast to vdom.
/** @type {import('unified').CompilerFunction<MdastRoot, VNode>} */
function compiler(node) {
/** @type {HastNode} */
// @ts-expect-error: assume a root w/o doctypes.
let hast = div(toHast(node).children)

if (clean) {
hast = sanitize(hast, schema)
hast = sanitize(hast, schema || undefined)

// If `div` is removed by sanitation, add it back.
if (hast.type === 'root') {
// @ts-expect-error: assume a root w/o doctypes.
hast = div(hast.children)
}
}

return toH(w, hast, settings.prefix)
// @ts-expect-error: assume no doctypes.
return toH(w, hast, options.prefix)
}

// Wrapper around `h` to pass components in.
/**
* Wrapper around `h` to pass components in.
*
* @param {string} name
* @param {object} props
* @param {VChild[]|undefined} children
* @returns {VNode|VNode[]}
*/
function w(name, props, children) {
const id = name.toLowerCase()
const fn = own.call(components, id) ? components[id] : h
return fn(name, props, children)
// @ts-expect-error: vdom types vague.
return fn(name, props, children || [])
}

// Wrap `children` in a hast div.
/**
* Wrap `children` in a hast div.
*
* @param {HastElement['children']} children
* @returns {HastElement}
*/
function div(children) {
return {
type: 'element',
tagName: 'div',
properties: {},
children
}
return {type: 'element', tagName: 'div', properties: {}, children}
}
}
19 changes: 18 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,41 @@
"sideEffects": false,
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.d.ts",
"index.js"
],
"dependencies": {
"@types/hast": "^2.3.2",
"@types/mdast": "^3.0.0",
"hast-to-hyperscript": "^10.0.0",
"hast-util-sanitize": "^4.0.0",
"mdast-util-to-hast": "^11.0.0",
"unified": "^10.0.0",
"virtual-dom": "^2.0.0"
},
"devDependencies": {
"@types/tape": "^4.0.0",
"@types/virtual-dom": "^2.1.1",
"c8": "^7.0.0",
"prettier": "^2.0.0",
"remark": "^14.0.0",
"remark-cli": "^10.0.0",
"remark-preset-wooorm": "^8.0.0",
"rimraf": "^3.0.0",
"tape": "^5.0.0",
"type-coverage": "^2.0.0",
"typescript": "^4.0.0",
"vdom-to-html": "^2.0.0",
"xo": "^0.39.0"
},
"scripts": {
"build": "rimraf \"*.d.ts\" && tsc && type-coverage",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node --conditions development test.js",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
"test": "npm run format && npm run test-coverage"
"test": "npm run build && npm run format && npm run test-coverage"
},
"prettier": {
"tabWidth": 2,
Expand All @@ -71,5 +82,11 @@
"plugins": [
"preset-wooorm"
]
},
"typeCoverage": {
"atLeast": 100,
"detail": true,
"strict": true,
"ignoreCatch": true
}
}
21 changes: 16 additions & 5 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
/**
* @typedef {import('./index.js').Options} Options
* @typedef {import('virtual-dom').VNode} VNode
*/

import test from 'tape'
import {remark} from 'remark'
import h from 'virtual-dom/h.js'
import {h} from 'virtual-dom'
// @ts-expect-error: untyped.
import vdom2html from 'vdom-to-html'
import vdom from './index.js'

test('remark-vdom', (t) => {
/**
* @param {string} [fixture]
* @param {Options} [options]
*/
function check(fixture, options) {
return vdom2html(remark().use(vdom, options).processSync(fixture).result)
}
Expand All @@ -23,6 +33,7 @@ test('remark-vdom', (t) => {

t.equal(
check('_Emphasis_!', {
// @ts-expect-error: TS tripping up, it’s fine.
h(name, props, children) {
return h(name === 'EM' ? 'I' : name, props, children)
}
Expand Down Expand Up @@ -50,7 +61,7 @@ test('remark-vdom', (t) => {
t.equal(
check('_Emphasis_!', {
components: {
em(name, props, children) {
em(_, _1, children) {
return children
}
}
Expand All @@ -59,9 +70,9 @@ test('remark-vdom', (t) => {
'`components`'
)

const node = remark()
.use(vdom, {prefix: 'f-'})
.processSync('_Emphasis_!').result
const node = /** @type {VNode} */ (
remark().use(vdom, {prefix: 'f-'}).processSync('_Emphasis_!').result
)

t.equal(node.key, 'f-1', '`prefix`')

Expand Down
16 changes: 16 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"include": ["*.js"],
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ES2020",
"moduleResolution": "node",
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"strict": true
}
}

0 comments on commit d86dd92

Please sign in to comment.