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
|
31 | import Parser5 from 'parse5/lib/parser/index.js'
|
32 | import {fromParse5} from 'hast-util-from-parse5'
|
33 | import {errors} from './errors.js'
|
34 |
|
35 | const base = 'https://html.spec.whatwg.org/multipage/parsing.html#parse-error-'
|
36 |
|
37 | const fatalities = {2: true, 1: false, 0: null}
|
38 |
|
39 | /** @type {import('unified').Plugin<[Options?] | void[], string, Root>} */
|
40 | export default function rehypeParse(options) {
|
41 | const processorSettings = /** @type {Options} */ (this.data('settings'))
|
42 | const settings = Object.assign({}, options, processorSettings)
|
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 | */
|
120 | function 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 | }
|