UNPKG

4.37 kBJavaScriptView Raw
1/**
2 * @typedef {import('hast').Root} Root
3 * @typedef {Pick<import('hast-util-from-parse5').Options, 'space' | 'verbose'>} FromParse5Options
4 *
5 * @typedef {keyof errors} ErrorCode
6 * @typedef {0|1|2|boolean|null|undefined} ErrorSeverity
7 * @typedef {Partial<Record<ErrorCode, ErrorSeverity>>} ErrorFields
8 *
9 * @typedef ParseFields
10 * @property {boolean|undefined} [fragment=false]
11 * Specify whether to parse a fragment, instead of a complete document.
12 * In document mode, unopened `html`, `head`, and `body` elements are opened
13 * in just the right places.
14 * @property {boolean|undefined} [emitParseErrors=false]
15 * > ⚠️ Parse errors are currently being added to HTML.
16 * > Not all errors emitted by parse5 (or rehype-parse) are specced yet.
17 * > Some documentation may still be missing.
18 *
19 * Emit parse errors while parsing on the vfile.
20 * Setting this to `true` starts emitting HTML parse errors.
21 *
22 * Specific rules can be turned off by setting them to `false` (or `0`).
23 * The default, when `emitParseErrors: true`, is `true` (or `1`), and means
24 * that rules emit as warnings.
25 * Rules can also be configured with `2`, to turn them into fatal errors.
26 *
27 * @typedef {FromParse5Options & ParseFields & ErrorFields} Options
28 */
29
30// @ts-expect-error: remove when typed
31import Parser5 from 'parse5/lib/parser/index.js'
32import {fromParse5} from 'hast-util-from-parse5'
33import {errors} from './errors.js'
34
35const base = 'https://html.spec.whatwg.org/multipage/parsing.html#parse-error-'
36
37const fatalities = {2: true, 1: false, 0: null}
38
39/** @type {import('unified').Plugin<[Options?] | void[], string, Root>} */
40export default function rehypeParse(options) {
41 const processorSettings = /** @type {Options} */ (this.data('settings'))
42 const settings = Object.assign({}, processorSettings, options)
43
44 Object.assign(this, {Parser: parser})
45
46 /** @type {import('unified').ParserFunction<Root>} */
47 function parser(doc, file) {
48 const fn = settings.fragment ? 'parseFragment' : 'parse'
49 const onParseError = settings.emitParseErrors ? onerror : null
50 const parse5 = new Parser5({
51 sourceCodeLocationInfo: true,
52 onParseError,
53 scriptingEnabled: false
54 })
55
56 // @ts-expect-error: `parse5` returns document or fragment, which are always
57 // mapped to roots.
58 return fromParse5(parse5[fn](doc), {
59 space: settings.space,
60 file,
61 verbose: settings.verbose
62 })
63
64 /**
65 * @param {{code: string, startLine: number, startCol: number, startOffset: number, endLine: number, endCol: number, endOffset: number}} error
66 */
67 function onerror(error) {
68 const code = error.code
69 const name = camelcase(code)
70 const setting = settings[name]
71 const config = setting === undefined || setting === null ? true : setting
72 const level = typeof config === 'number' ? config : config ? 1 : 0
73 const start = {
74 line: error.startLine,
75 column: error.startCol,
76 offset: error.startOffset
77 }
78 const end = {
79 line: error.endLine,
80 column: error.endCol,
81 offset: error.endOffset
82 }
83 if (level) {
84 /* c8 ignore next */
85 const info = errors[name] || {reason: '', description: '', url: ''}
86 const message = file.message(format(info.reason), {start, end})
87 message.source = 'parse-error'
88 message.ruleId = code
89 message.fatal = fatalities[level]
90 message.note = format(info.description)
91 message.url = 'url' in info && info.url === false ? null : base + code
92 }
93
94 /**
95 * @param {string} value
96 * @returns {string}
97 */
98 function format(value) {
99 return value
100 .replace(/%c(?:-(\d+))?/g, (_, /** @type {string} */ $1) => {
101 const offset = $1 ? -Number.parseInt($1, 10) : 0
102 const char = doc.charAt(error.startOffset + offset)
103 return char === '`' ? '` ` `' : char
104 })
105 .replace(
106 /%x/g,
107 () =>
108 '0x' +
109 doc.charCodeAt(error.startOffset).toString(16).toUpperCase()
110 )
111 }
112 }
113 }
114}
115
116/**
117 * @param {string} value
118 * @returns {ErrorCode}
119 */
120function camelcase(value) {
121 // @ts-expect-error: this returns a valid error code.
122 return value.replace(/-[a-z]/g, ($0) => $0.charAt(1).toUpperCase())
123}