Skip to content

Commit 10fa4b2

Browse files
committed
.
0 parents  commit 10fa4b2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1216
-0
lines changed

.editorconfig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_size = 2
7+
indent_style = space
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true

.github/workflows/bb.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
jobs:
2+
main:
3+
runs-on: ubuntu-latest
4+
steps:
5+
- uses: unifiedjs/beep-boop-beta@main
6+
with:
7+
repo-token: ${{secrets.GITHUB_TOKEN}}
8+
name: bb
9+
on:
10+
issues:
11+
types: [closed, edited, labeled, opened, reopened, unlabeled]
12+
pull_request_target:
13+
types: [closed, edited, labeled, opened, reopened, unlabeled]

.github/workflows/main.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
jobs:
2+
main:
3+
name: ${{matrix.node}}
4+
runs-on: ubuntu-latest
5+
steps:
6+
- uses: actions/checkout@v4
7+
- uses: actions/setup-node@v4
8+
with:
9+
node-version: ${{matrix.node}}
10+
- run: npm install
11+
- run: npm test
12+
- uses: codecov/codecov-action@v4
13+
strategy:
14+
matrix:
15+
node:
16+
- lts/hydrogen
17+
- node
18+
name: main
19+
on:
20+
- pull_request
21+
- push

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.DS_Store
2+
*.d.ts.map
3+
*.d.ts
4+
*.log
5+
*.tsbuildinfo
6+
coverage/
7+
node_modules/
8+
yarn.lock
9+
!/lib/types.d.ts
10+
!/index.d.ts

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ignore-scripts=true
2+
package-lock=false

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
coverage/
2+
*.html
3+
*.md

index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export type {Options} from './lib/types.js'
2+
export {format} from './lib/index.js'

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Note: types exposed from `index.d.ts`.
2+
export {format} from './lib/index.js'

lib/index.js

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* @import {Nodes, RootContent, Root} from 'hast'
3+
* @import {BuildVisitor} from 'unist-util-visit-parents'
4+
* @import {Options, State} from './types.js'
5+
*/
6+
7+
import {embedded} from 'hast-util-embedded'
8+
import {minifyWhitespace} from 'hast-util-minify-whitespace'
9+
import {phrasing} from 'hast-util-phrasing'
10+
import {whitespace} from 'hast-util-whitespace'
11+
import {whitespaceSensitiveTagNames} from 'html-whitespace-sensitive-tag-names'
12+
import {SKIP, visitParents} from 'unist-util-visit-parents'
13+
14+
/** @type {Options} */
15+
const emptyOptions = {}
16+
17+
/**
18+
* Format whitespace in HTML.
19+
*
20+
* @param {Root} tree
21+
* Tree.
22+
* @param {Options | null | undefined} [options]
23+
* Configuration (optional).
24+
* @returns {undefined}
25+
* Nothing.
26+
*/
27+
export function format(tree, options) {
28+
const settings = options || emptyOptions
29+
30+
/** @type {State} */
31+
const state = {
32+
blanks: settings.blanks || [],
33+
head: false,
34+
indentInitial: settings.indentInitial !== false,
35+
indent:
36+
typeof settings.indent === 'number'
37+
? ' '.repeat(settings.indent)
38+
: settings.indent || ' '
39+
}
40+
41+
minifyWhitespace(tree, {newlines: true})
42+
43+
visitParents(tree, visitor)
44+
45+
/**
46+
* @type {BuildVisitor<Root>}
47+
*/
48+
function visitor(node, parents) {
49+
if (!('children' in node)) {
50+
return
51+
}
52+
53+
if (node.type === 'element' && node.tagName === 'head') {
54+
state.head = true
55+
}
56+
57+
if (state.head && node.type === 'element' && node.tagName === 'body') {
58+
state.head = false
59+
}
60+
61+
if (
62+
node.type === 'element' &&
63+
whitespaceSensitiveTagNames.includes(node.tagName)
64+
) {
65+
return SKIP
66+
}
67+
68+
// Don’t indent content of whitespace-sensitive nodes / inlines.
69+
if (node.children.length === 0 || !padding(state, node)) {
70+
return
71+
}
72+
73+
let level = parents.length
74+
75+
if (!state.indentInitial) {
76+
level--
77+
}
78+
79+
let eol = false
80+
81+
// Indent newlines in `text`.
82+
for (const child of node.children) {
83+
if (child.type === 'comment' || child.type === 'text') {
84+
if (child.value.includes('\n')) {
85+
eol = true
86+
}
87+
88+
child.value = child.value.replace(
89+
/ *\n/g,
90+
'$&' + state.indent.repeat(level)
91+
)
92+
}
93+
}
94+
95+
/** @type {Array<RootContent>} */
96+
const result = []
97+
/** @type {RootContent | undefined} */
98+
let previous
99+
100+
for (const child of node.children) {
101+
if (padding(state, child) || (eol && !previous)) {
102+
addBreak(result, level, child)
103+
eol = true
104+
}
105+
106+
previous = child
107+
result.push(child)
108+
}
109+
110+
if (previous && (eol || padding(state, previous))) {
111+
// Ignore trailing whitespace (if that already existed), as we’ll add
112+
// properly indented whitespace.
113+
if (whitespace(previous)) {
114+
result.pop()
115+
previous = result[result.length - 1]
116+
}
117+
118+
addBreak(result, level - 1)
119+
}
120+
121+
node.children = result
122+
}
123+
124+
/**
125+
* @param {Array<RootContent>} list
126+
* Nodes.
127+
* @param {number} level
128+
* Indentation level.
129+
* @param {RootContent | undefined} [next]
130+
* Next node.
131+
* @returns {undefined}
132+
* Nothing.
133+
*/
134+
function addBreak(list, level, next) {
135+
const tail = list[list.length - 1]
136+
const previous = tail && whitespace(tail) ? list[list.length - 2] : tail
137+
const replace =
138+
(blank(state, previous) && blank(state, next) ? '\n\n' : '\n') +
139+
state.indent.repeat(Math.max(level, 0))
140+
141+
if (tail && tail.type === 'text') {
142+
tail.value = whitespace(tail) ? replace : tail.value + replace
143+
} else {
144+
list.push({type: 'text', value: replace})
145+
}
146+
}
147+
}
148+
149+
/**
150+
* @param {State} state
151+
* Info passed around.
152+
* @param {Nodes | undefined} node
153+
* Node.
154+
* @returns {boolean}
155+
* Whether `node` is a blank.
156+
*/
157+
function blank(state, node) {
158+
return Boolean(
159+
node &&
160+
node.type === 'element' &&
161+
state.blanks.length > 0 &&
162+
state.blanks.includes(node.tagName)
163+
)
164+
}
165+
166+
/**
167+
* @param {State} state
168+
* Info passed around.
169+
* @param {Nodes} node
170+
* Node.
171+
* @returns {boolean}
172+
* Whether `node` should be padded.
173+
*/
174+
function padding(state, node) {
175+
return (
176+
node.type === 'root' ||
177+
(node.type === 'element'
178+
? state.head ||
179+
node.tagName === 'script' ||
180+
embedded(node) ||
181+
!phrasing(node)
182+
: false)
183+
)
184+
}

lib/types.d.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Configuration.
3+
*/
4+
export interface Options {
5+
/**
6+
* List of tag names to join with a blank line (default: `[]`);
7+
* these tags,
8+
* when next to each other,
9+
* are joined by a blank line (`\n\n`);
10+
* for example,
11+
* when `['head', 'body']` is given,
12+
* a blank line is added between these two.
13+
*/
14+
blanks?: Array<string> | null | undefined
15+
/**
16+
* Whether to indent the first level (default: `true`);
17+
* this is usually the `<html>`,
18+
* thus not indenting `head` and `body`.
19+
*/
20+
indentInitial?: boolean | null | undefined
21+
/**
22+
* Indentation per level (default: `2`);
23+
* when `number`,
24+
* uses that amount of spaces; when `string`,
25+
* uses that per indentation level.
26+
*/
27+
indent?: number | string | null | undefined
28+
}
29+
30+
/**
31+
* State.
32+
*/
33+
export interface State {
34+
/**
35+
* List of tag names to join with a blank line.
36+
*/
37+
blanks: Array<string>
38+
/**
39+
* Whether the node is in `head`.
40+
*/
41+
head: boolean
42+
/**
43+
* Whether to indent the first level.
44+
*/
45+
indentInitial: boolean
46+
/**
47+
* Indentation per level.
48+
*/
49+
indent: string
50+
}

lib/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Note: types only.
2+
export {}

license

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
(The MIT License)
2+
3+
Copyright (c) Titus Wormer <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
'Software'), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

0 commit comments

Comments
 (0)