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 1, 2021
1 parent f2298e0 commit f9e30d5
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 36 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
99 changes: 74 additions & 25 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
/**
* @typedef {import('hast').Root} Root
* @typedef {Root['children'][number]} Child
* @typedef {import('hast').Element} Element
* @typedef {Root|Child} Node
*
* @typedef Options
* Configuration.
* @property {number|string} [indent=2]
* Indentation per level (`number`, `string`, default: `2`).
* When number, uses that amount of spaces.
* When `string`, uses that per indentation level.
* @property {boolean} [indentInitial=true]
* Whether to indent the first level (`boolean`, default: `true`).
* This is usually the `<html>`, thus not indenting `head` and `body`.
* @property {string[]} [blanks=[]]
* List of tag names to join with a blank line (`Array.<string>`, default:
* `[]`).
* These tags, when next to each other, are joined by a blank line (`\n\n`).
* For example, when `['head', 'body']` is given, a blank line is added
* between these two.
*/

import rehypeMinifyWhitespace from 'rehype-minify-whitespace'
import {visitParents, SKIP} from 'unist-util-visit-parents'
import {embedded} from 'hast-util-embedded'
import {phrasing} from 'hast-util-phrasing'
import {whitespace} from 'hast-util-whitespace'
import {isElement} from 'hast-util-is-element'
import {whitespaceSensitiveTagNames} from 'html-whitespace-sensitive-tag-names'
// @ts-expect-error: to do remove.
import repeat from 'repeat-string'

const minify = rehypeMinifyWhitespace({newlines: true})

export default function rehypeFormat(options) {
const settings = options || {}
let indent = settings.indent || 2
let indentInitial = settings.indentInitial
/**
* @type {import('unified').Plugin<[Options?] | void[], Root>}
*/
export default function rehypeFormat(options = {}) {
let indent = options.indent || 2
let indentInitial = options.indentInitial

if (typeof indent === 'number') {
indent = repeat(' ', indent)
Expand All @@ -23,32 +49,36 @@ export default function rehypeFormat(options) {
indentInitial = true
}

return transform

function transform(tree) {
return (tree) => {
/** @type {boolean|undefined} */
let head

// @ts-expect-error: fine, it’s a sync transformer.
minify(tree)

visitParents(tree, visitor)

function visitor(node, parents) {
const children = node.children || []
let level = parents.length
// eslint-disable-next-line complexity
visitParents(tree, (node, parents) => {
let index = -1

if (!('children' in node)) {
return
}

if (isElement(node, 'head')) {
head = true
}

if (head && isElement(node, 'body')) {
head = null
head = undefined
}

if (isElement(node, whitespaceSensitiveTagNames)) {
return SKIP
}

const children = node.children
let level = parents.length

// Don’t indent content of whitespace-sensitive nodes / inlines.
if (children.length === 0 || !padding(node, head)) {
return
Expand All @@ -58,6 +88,7 @@ export default function rehypeFormat(options) {
level--
}

/** @type {boolean|undefined} */
let eol

// Indent newlines in `text`.
Expand All @@ -76,8 +107,11 @@ export default function rehypeFormat(options) {
}
}

/** @type {Child[]} */
const result = []
/** @type {Child|undefined} */
let previous

index = -1

while (++index < children.length) {
Expand All @@ -92,7 +126,7 @@ export default function rehypeFormat(options) {
result.push(child)
}

if (eol || padding(previous, head)) {
if (previous && (eol || padding(previous, head))) {
// Ignore trailing whitespace (if that already existed), as we’ll add
// properly indented whitespace.
if (whitespace(previous)) {
Expand All @@ -104,19 +138,15 @@ export default function rehypeFormat(options) {
}

node.children = result
}
}

function blank(node) {
return (
node &&
node.type === 'element' &&
settings.blanks &&
settings.blanks.length > 0 &&
settings.blanks.includes(node.tagName)
)
})
}

/**
* @param {Child[]} list
* @param {number} level
* @param {Child} [next]
* @returns {void}
*/
function addBreak(list, level, next) {
const tail = list[list.length - 1]
const previous = whitespace(tail) ? list[list.length - 2] : tail
Expand All @@ -129,8 +159,27 @@ export default function rehypeFormat(options) {
list.push({type: 'text', value: replace})
}
}

/**
* @param {Node|undefined} node
* @returns {boolean}
*/
function blank(node) {
return Boolean(
node &&
node.type === 'element' &&
options.blanks &&
options.blanks.length > 0 &&
options.blanks.includes(node.tagName)
)
}
}

/**
* @param {Node} node
* @param {boolean|undefined} head
* @returns {boolean}
*/
function padding(node, head) {
return (
node.type === 'root' ||
Expand Down
18 changes: 16 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,44 @@
"sideEffects": false,
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.d.ts",
"index.js"
],
"dependencies": {
"@types/hast": "^2.3.2",
"hast-util-embedded": "^2.0.0",
"hast-util-is-element": "^2.0.0",
"hast-util-phrasing": "^2.0.0",
"hast-util-whitespace": "^2.0.0",
"html-whitespace-sensitive-tag-names": "^2.0.0",
"rehype-minify-whitespace": "^5.0.0",
"repeat-string": "^1.0.0",
"unified": "^10.0.0",
"unist-util-visit-parents": "^5.0.0"
},
"devDependencies": {
"@types/tape": "^4.0.0",
"c8": "^7.0.0",
"is-hidden": "^2.0.0",
"negate": "^1.0.0",
"prettier": "^2.0.0",
"rehype": "^12.0.0",
"remark-cli": "^9.0.0",
"remark-preset-wooorm": "^8.0.0",
"rimraf": "^3.0.0",
"tape": "^5.0.0",
"to-vfile": "^7.0.0",
"type-coverage": "^2.0.0",
"typescript": "^4.0.0",
"xo": "^0.38.0"
},
"scripts": {
"build": "rimraf \"*.d.ts\" \"test/**/*.d.ts\" && tsc && type-coverage",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node --conditions development test/index.js",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --conditions development test/index.js",
"test": "npm run format && npm run test-coverage"
"test": "npm run build && npm run format && npm run test-coverage"
},
"prettier": {
"tabWidth": 2,
Expand All @@ -73,5 +81,11 @@
"plugins": [
"preset-wooorm"
]
},
"typeCoverage": {
"atLeast": 100,
"detail": true,
"strict": true,
"ignoreCatch": true
}
}
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Say we have the following file, `index.html`:
</html>
```

And our script, `example.js`, looks as follows:
And our module, `example.js`, looks as follows:

```js
import {readSync} from 'to-vfile'
Expand Down Expand Up @@ -84,7 +84,7 @@ The default export is `rehypeFormat`.

### `unified().use(rehypeFormat[, options])`

Format white space in the processed tree.
Format whitespace in the processed tree.

* Collapse all white space (to a single space or newline)
* Remove unneeded white space
Expand Down
17 changes: 10 additions & 7 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
/**
* @typedef {import('../index.js').Options} Options
*/

import fs from 'fs'
import path from 'path'
import test from 'tape'
import {rehype} from 'rehype'
import {readSync} from 'to-vfile'
import negate from 'negate'
import {isHidden} from 'is-hidden'
import fmt from '../index.js'

test('format', (t) => {
const root = path.join('test', 'fixtures')

const files = fs.readdirSync(root).filter(negate(isHidden))
const files = fs.readdirSync(root).filter((d) => !isHidden(d))

t.plan(files.length)

let index = -1
while (++index < files.length) {
one(files[index])
}

function one(fixture) {
const fixture = files[index]
const base = path.join(root, fixture)
const input = readSync(path.join(base, 'input.html'))
const output = readSync(path.join(base, 'output.html'))
/** @type {Options|undefined} */
let config

try {
config = JSON.parse(fs.readFileSync(path.join(base, 'config.json')))
config = JSON.parse(
String(fs.readFileSync(path.join(base, 'config.json')))
)
} catch {}

const proc = rehype().use(fmt, config)
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", "test/**/*.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 f9e30d5

Please sign in to comment.