Skip to content

Commit e9a82d0

Browse files
committed
docs: annotate public api
1 parent 604a6ef commit e9a82d0

7 files changed

Lines changed: 207 additions & 37 deletions

File tree

‎src/config.ts‎

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,31 @@
33
* @since 2017-08-19 15:56:14
44
*/
55

6-
function createMap<T>(keys: string, value: T): Record<string, T> {
7-
return keys.split(',').reduce((pre, now) => {
8-
pre[now] = value;
9-
return pre;
10-
}, Object.create(null));
11-
}
12-
13-
export const selfCloseTags = createMap<true>(
14-
'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr,!doctype,,!,!--',
15-
true,
16-
);
6+
/**
7+
* HTML tags that do not require closing tags.
8+
*/
9+
export const selfCloseTags = new Set([
10+
'area',
11+
'base',
12+
'br',
13+
'col',
14+
'embed',
15+
'hr',
16+
'img',
17+
'input',
18+
'link',
19+
'meta',
20+
'param',
21+
'source',
22+
'track',
23+
'wbr',
24+
'!doctype',
25+
'',
26+
'!',
27+
'!--',
28+
]);
1729

18-
export const noNestedTags = createMap<true>('li,option,select,textarea', true);
30+
/**
31+
* Tags that implicitly close a previous tag of the same name.
32+
*/
33+
export const noNestedTags = new Set(['li', 'option', 'select', 'textarea']);

‎src/parse.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ interface IContext {
1616
tag: ITag;
1717
}
1818

19+
/**
20+
* Options that control AST construction.
21+
*/
1922
export interface ParseOptions {
20-
// create tag's attributes map
21-
// if true, will set ITag.attributeMap property
22-
// as a `Record<string, IAttribute>`
23+
/**
24+
* Populate each tag node with an attribute lookup map.
25+
*/
2326
setAttributeMap: boolean;
2427
}
2528

@@ -59,7 +62,7 @@ function pushNode(_node: ITag | IText) {
5962
} else if (
6063
_node.type === SyntaxKind.Tag &&
6164
_node.name === tagChain.tag.name &&
62-
noNestedTags[_node.name]
65+
noNestedTags.has(_node.name)
6366
) {
6467
tagChain = tagChain.parent;
6568
pushNode(_node);
@@ -185,7 +188,7 @@ function parseOpenTag() {
185188
if (token.type === TokenKind.OpenTagEnd) {
186189
tag.end = tag.open.end = token.end + 1;
187190
tag.open.value = buffer.substring(tag.open.start, tag.open.end);
188-
if (token.value === '' && !selfCloseTags[tag.name]) {
191+
if (token.value === '' && !selfCloseTags.has(tag.name)) {
189192
tag.body = [];
190193
pushTagChain(tag);
191194
} else {
@@ -258,6 +261,9 @@ function parseCloseTag() {
258261
tagChain = _context;
259262
}
260263

264+
/**
265+
* Parse an HTML string into an AST.
266+
*/
261267
export function parse(input: string, options?: ParseOptions): INode[] {
262268
init(input, {
263269
setAttributeMap: false,

‎src/safeHtml.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,31 @@ import { parse } from './parse';
88
import { SyntaxKind } from './types';
99
import type { INode } from './types';
1010

11+
/**
12+
* Options used to sanitize and stringify HTML.
13+
*/
1114
export interface SafeHtmlOptions {
15+
/**
16+
* Tag names allowed in the sanitized output.
17+
*/
1218
allowedTags: string[];
19+
/**
20+
* Attribute names allowed on every allowed tag.
21+
*/
1322
allowedAttrs: string[];
23+
/**
24+
* Attribute names allowed only for specific tags.
25+
*/
1426
tagAllowedAttrs: Record<string, string[]>;
27+
/**
28+
* URL pattern allowed for href and src attributes.
29+
*/
1530
allowedUrl: RegExp;
1631
}
1732

33+
/**
34+
* Default safe HTML allowlist.
35+
*/
1836
export const safeHtmlDefaultOptions: SafeHtmlOptions = {
1937
allowedTags: [
2038
'a',
@@ -106,6 +124,9 @@ export const safeHtmlDefaultOptions: SafeHtmlOptions = {
106124
allowedUrl: /^(?:mailto|tel|https?|ftp|[^:]*[^a-z0-9.+-][^:]*):|^[^:]*$/i,
107125
};
108126

127+
/**
128+
* Sanitize an HTML string using the configured allowlist.
129+
*/
109130
export function safeHtml(input: string, options: Partial<SafeHtmlOptions> = {}): string {
110131
const config: SafeHtmlOptions = {
111132
...safeHtmlDefaultOptions,
@@ -128,7 +149,7 @@ function stringify(ast: INode[], config: SafeHtmlOptions, input: string): string
128149
if (config.allowedTags.indexOf(node.name) === -1) {
129150
return '';
130151
}
131-
if (selfCloseTags[node.name]) {
152+
if (selfCloseTags.has(node.name)) {
132153
if (node.body !== void 0) {
133154
throw new Error(`self closed tag "${node.name}" should not have body`);
134155
}

‎src/tokenize.ts‎

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,67 @@ const enum State {
2121
ClosingTag,
2222
}
2323

24+
/**
25+
* Token kinds emitted by tokenize.
26+
*/
2427
export const enum TokenKind {
28+
/**
29+
* Raw text outside tag syntax.
30+
*/
2531
Literal,
26-
OpenTag, // trim leading '<'
27-
OpenTagEnd, // trim tailing '>', only could be '/' or ''
28-
CloseTag, // trim leading '</' and tailing '>'
29-
Whitespace, // the whitespace between attributes
32+
/**
33+
* Opening tag name without the leading <.
34+
*/
35+
OpenTag,
36+
/**
37+
* Opening tag end marker without the trailing >; value is / or empty.
38+
*/
39+
OpenTagEnd,
40+
/**
41+
* Closing tag name without the leading </ or trailing >.
42+
*/
43+
CloseTag,
44+
/**
45+
* Whitespace between attributes.
46+
*/
47+
Whitespace,
48+
/**
49+
* Attribute value assignment marker.
50+
*/
3051
AttrValueEq,
52+
/**
53+
* Unquoted attribute value.
54+
*/
3155
AttrValueNq,
56+
/**
57+
* Single-quoted attribute value.
58+
*/
3259
AttrValueSq,
60+
/**
61+
* Double-quoted attribute value.
62+
*/
3363
AttrValueDq,
3464
}
3565

66+
/**
67+
* Token emitted by the HTML tokenizer.
68+
*/
3669
export interface IToken {
70+
/**
71+
* Zero-based start offset in the source string.
72+
*/
3773
start: number;
74+
/**
75+
* Zero-based end offset in the source string.
76+
*/
3877
end: number;
78+
/**
79+
* Raw token value after token-specific trimming.
80+
*/
3981
value: string;
82+
/**
83+
* Token category.
84+
*/
4085
type: TokenKind;
4186
}
4287

@@ -115,6 +160,9 @@ function init(input: string) {
115160
offset = 0;
116161
}
117162

163+
/**
164+
* Convert an HTML string into a flat token stream.
165+
*/
118166
export function tokenize(input: string): IToken[] {
119167
init(input);
120168
while (index < bufSize) {

‎src/types.ts‎

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,115 @@
33
* @since 2017-08-18 15:17:51
44
*/
55

6+
/**
7+
* Node kinds returned by the parser.
8+
*/
69
export enum SyntaxKind {
10+
/**
11+
* Plain text content.
12+
*/
713
Text = 'Text',
14+
/**
15+
* HTML tag node.
16+
*/
817
Tag = 'Tag',
918
}
1019

20+
/**
21+
* Common source range shared by every AST node.
22+
*/
1123
export interface IBaseNode {
24+
/**
25+
* Zero-based start offset in the source string.
26+
*/
1227
start: number;
28+
/**
29+
* Zero-based end offset in the source string.
30+
*/
1331
end: number;
1432
}
1533

34+
/**
35+
* Text node in the parsed AST.
36+
*/
1637
export interface IText extends IBaseNode {
38+
/**
39+
* Discriminator for text nodes.
40+
*/
1741
type: SyntaxKind.Text;
42+
/**
43+
* Original text content.
44+
*/
1845
value: string;
1946
}
2047

48+
/**
49+
* Parsed attribute value, with quote information preserved.
50+
*/
2151
export interface IAttributeValue extends IBaseNode {
52+
/**
53+
* Attribute value without surrounding quotes.
54+
*/
2255
value: string;
56+
/**
57+
* Quote character used by the source value, or undefined for unquoted values.
58+
*/
2359
quote: "'" | '"' | undefined;
2460
}
2561

62+
/**
63+
* Parsed tag attribute.
64+
*/
2665
export interface IAttribute extends IBaseNode {
66+
/**
67+
* Attribute name token.
68+
*/
2769
name: IText;
70+
/**
71+
* Attribute value, if the source provided one.
72+
*/
2873
value: IAttributeValue | undefined;
2974
}
3075

76+
/**
77+
* Tag node in the parsed AST.
78+
*/
3179
export interface ITag extends IBaseNode {
80+
/**
81+
* Discriminator for tag nodes.
82+
*/
3283
type: SyntaxKind.Tag;
33-
// original open tag, <Div id="id">
84+
/**
85+
* Original opening tag text, such as <Div id="id">.
86+
*/
3487
open: IText;
35-
// lower case tag name, div
88+
/**
89+
* Lowercase tag name, such as div.
90+
*/
3691
name: string;
37-
// original case tag name, Div
92+
/**
93+
* Tag name with original casing, such as Div.
94+
*/
3895
rawName: string;
96+
/**
97+
* Attributes in source order.
98+
*/
3999
attributes: IAttribute[];
40-
// the attribute map, if `options.setAttributeMap` is `true`
41-
// this will be a Record, key is the attribute name literal,
42-
// value is the attribute self.
100+
/**
101+
* Attribute lookup map when ParseOptions.setAttributeMap is true.
102+
*/
43103
attributeMap: Record<string, IAttribute> | undefined;
44-
body:
45-
| Array<ITag | IText> // with close tag
46-
| undefined // self closed
47-
| null; // EOF before open tag end
48-
// original close tag, </DIV >
49-
close:
50-
| IText // with close tag
51-
| undefined // self closed
52-
| null; // EOF before end or without close tag
104+
/**
105+
* Child nodes for paired tags, undefined for self-closed tags, or null when the opening tag is incomplete.
106+
*/
107+
body: Array<ITag | IText> | undefined | null;
108+
/**
109+
* Original closing tag text, undefined for self-closed tags, or null when missing.
110+
*/
111+
close: IText | undefined | null;
53112
}
54113

114+
/**
115+
* Any node returned by parse.
116+
*/
55117
export type INode = IText | ITag;

‎src/utils.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
* @since 2017-08-20 21:41:39
44
*/
55

6+
/**
7+
* Build cumulative line ending offsets for source position lookup.
8+
*/
69
export function getLineRanges(input: string) {
710
return input.split('\n').reduce(
811
(arr, line) => {
@@ -13,6 +16,9 @@ export function getLineRanges(input: string) {
1316
);
1417
}
1518

19+
/**
20+
* Convert a source offset to one-based line and column coordinates.
21+
*/
1622
export function getPosition(ranges: number[], offset: number): [number, number] {
1723
let line = NaN;
1824
let column = NaN;

0 commit comments

Comments
 (0)