AK-21/Graphite-Industrial-Intelligence
0
1/**2 * @import {Point} from 'unist'3 * @import {Options} from '../index.js'4 */5 6import {characterEntitiesLegacy} from 'character-entities-legacy'7import {characterReferenceInvalid} from 'character-reference-invalid'8import {isDecimal} from 'is-decimal'9import {isHexadecimal} from 'is-hexadecimal'10import {isAlphanumerical} from 'is-alphanumerical'11import {decodeNamedCharacterReference} from 'decode-named-character-reference'12 13// Warning messages.14const messages = [15 '',16 /* 1: Non terminated (named) */17 'Named character references must be terminated by a semicolon',18 /* 2: Non terminated (numeric) */19 'Numeric character references must be terminated by a semicolon',20 /* 3: Empty (named) */21 'Named character references cannot be empty',22 /* 4: Empty (numeric) */23 'Numeric character references cannot be empty',24 /* 5: Unknown (named) */25 'Named character references must be known',26 /* 6: Disallowed (numeric) */27 'Numeric character references cannot be disallowed',28 /* 7: Prohibited (numeric) */29 'Numeric character references cannot be outside the permissible Unicode range'30]31 32/**33 * Parse HTML character references.34 *35 * @param {string} value36 * @param {Readonly<Options> | null | undefined} [options]37 */38export function parseEntities(value, options) {39 const settings = options || {}40 const additional =41 typeof settings.additional === 'string'42 ? settings.additional.charCodeAt(0)43 : settings.additional44 /** @type {Array<string>} */45 const result = []46 let index = 047 let lines = -148 let queue = ''49 /** @type {Point | undefined} */50 let point51 /** @type {Array<number>|undefined} */52 let indent53 54 if (settings.position) {55 if ('start' in settings.position || 'indent' in settings.position) {56 // @ts-expect-error: points don’t have indent.57 indent = settings.position.indent58 // @ts-expect-error: points don’t have indent.59 point = settings.position.start60 } else {61 point = settings.position62 }63 }64 65 let line = (point ? point.line : 0) || 166 let column = (point ? point.column : 0) || 167 68 // Cache the current point.69 let previous = now()70 /** @type {number|undefined} */71 let character72 73 // Ensure the algorithm walks over the first character (inclusive).74 index--75 76 while (++index <= value.length) {77 // If the previous character was a newline.78 if (character === 10 /* `\n` */) {79 column = (indent ? indent[lines] : 0) || 180 }81 82 character = value.charCodeAt(index)83 84 if (character === 38 /* `&` */) {85 const following = value.charCodeAt(index + 1)86 87 // The behavior depends on the identity of the next character.88 if (89 following === 9 /* `\t` */ ||90 following === 10 /* `\n` */ ||91 following === 12 /* `\f` */ ||92 following === 32 /* ` ` */ ||93 following === 38 /* `&` */ ||94 following === 60 /* `<` */ ||95 Number.isNaN(following) ||96 (additional && following === additional)97 ) {98 // Not a character reference.99 // No characters are consumed, and nothing is returned.100 // This is not an error, either.101 queue += String.fromCharCode(character)102 column++103 continue104 }105 106 const start = index + 1107 let begin = start108 let end = start109 /** @type {string} */110 let type111 112 if (following === 35 /* `#` */) {113 // Numerical reference.114 end = ++begin115 116 // The behavior further depends on the next character.117 const following = value.charCodeAt(end)118 119 if (following === 88 /* `X` */ || following === 120 /* `x` */) {120 // ASCII hexadecimal digits.121 type = 'hexadecimal'122 end = ++begin123 } else {124 // ASCII decimal digits.125 type = 'decimal'126 }127 } else {128 // Named reference.129 type = 'named'130 }131 132 let characterReferenceCharacters = ''133 let characterReference = ''134 let characters = ''135 // Each type of character reference accepts different characters.136 // This test is used to detect whether a reference has ended (as the semicolon137 // is not strictly needed).138 const test =139 type === 'named'140 ? isAlphanumerical141 : type === 'decimal'142 ? isDecimal143 : isHexadecimal144 145 end--146 147 while (++end <= value.length) {148 const following = value.charCodeAt(end)149 150 if (!test(following)) {151 break152 }153 154 characters += String.fromCharCode(following)155 156 // Check if we can match a legacy named reference.157 // If so, we cache that as the last viable named reference.158 // This ensures we do not need to walk backwards later.159 if (type === 'named' && characterEntitiesLegacy.includes(characters)) {160 characterReferenceCharacters = characters161 // @ts-expect-error: always able to decode.162 characterReference = decodeNamedCharacterReference(characters)163 }164 }165 166 let terminated = value.charCodeAt(end) === 59 /* `;` */167 168 if (terminated) {169 end++170 171 const namedReference =172 type === 'named' ? decodeNamedCharacterReference(characters) : false173 174 if (namedReference) {175 characterReferenceCharacters = characters176 characterReference = namedReference177 }178 }179 180 let diff = 1 + end - start181 let reference = ''182 183 if (!terminated && settings.nonTerminated === false) {184 // Empty.185 } else if (!characters) {186 // An empty (possible) reference is valid, unless it’s numeric (thus an187 // ampersand followed by an octothorp).188 if (type !== 'named') {189 warning(4 /* Empty (numeric) */, diff)190 }191 } else if (type === 'named') {192 // An ampersand followed by anything unknown, and not terminated, is193 // invalid.194 if (terminated && !characterReference) {195 warning(5 /* Unknown (named) */, 1)196 } else {197 // If there’s something after an named reference which is not known,198 // cap the reference.199 if (characterReferenceCharacters !== characters) {200 end = begin + characterReferenceCharacters.length201 diff = 1 + end - begin202 terminated = false203 }204 205 // If the reference is not terminated, warn.206 if (!terminated) {207 const reason = characterReferenceCharacters208 ? 1 /* Non terminated (named) */209 : 3 /* Empty (named) */210 211 if (settings.attribute) {212 const following = value.charCodeAt(end)213 214 if (following === 61 /* `=` */) {215 warning(reason, diff)216 characterReference = ''217 } else if (isAlphanumerical(following)) {218 characterReference = ''219 } else {220 warning(reason, diff)221 }222 } else {223 warning(reason, diff)224 }225 }226 }227 228 reference = characterReference229 } else {230 if (!terminated) {231 // All nonterminated numeric references are not rendered, and emit a232 // warning.233 warning(2 /* Non terminated (numeric) */, diff)234 }235 236 // When terminated and numerical, parse as either hexadecimal or237 // decimal.238 let referenceCode = Number.parseInt(239 characters,240 type === 'hexadecimal' ? 16 : 10241 )242 243 // Emit a warning when the parsed number is prohibited, and replace with244 // replacement character.245 if (prohibited(referenceCode)) {246 warning(7 /* Prohibited (numeric) */, diff)247 reference = String.fromCharCode(65533 /* `�` */)248 } else if (referenceCode in characterReferenceInvalid) {249 // Emit a warning when the parsed number is disallowed, and replace by250 // an alternative.251 warning(6 /* Disallowed (numeric) */, diff)252 reference = characterReferenceInvalid[referenceCode]253 } else {254 // Parse the number.255 let output = ''256 257 // Emit a warning when the parsed number should not be used.258 if (disallowed(referenceCode)) {259 warning(6 /* Disallowed (numeric) */, diff)260 }261 262 // Serialize the number.263 if (referenceCode > 0xffff) {264 referenceCode -= 0x10000265 output += String.fromCharCode(266 (referenceCode >>> (10 & 0x3ff)) | 0xd800267 )268 referenceCode = 0xdc00 | (referenceCode & 0x3ff)269 }270 271 reference = output + String.fromCharCode(referenceCode)272 }273 }274 275 // Found it!276 // First eat the queued characters as normal text, then eat a reference.277 if (reference) {278 flush()279 280 previous = now()281 index = end - 1282 column += end - start + 1283 result.push(reference)284 const next = now()285 next.offset++286 287 if (settings.reference) {288 settings.reference.call(289 settings.referenceContext || undefined,290 reference,291 {start: previous, end: next},292 value.slice(start - 1, end)293 )294 }295 296 previous = next297 } else {298 // If we could not find a reference, queue the checked characters (as299 // normal characters), and move the pointer to their end.300 // This is possible because we can be certain neither newlines nor301 // ampersands are included.302 characters = value.slice(start - 1, end)303 queue += characters304 column += characters.length305 index = end - 1306 }307 } else {308 // Handle anything other than an ampersand, including newlines and EOF.309 if (character === 10 /* `\n` */) {310 line++311 lines++312 column = 0313 }314 315 if (Number.isNaN(character)) {316 flush()317 } else {318 queue += String.fromCharCode(character)319 column++320 }321 }322 }323 324 // Return the reduced nodes.325 return result.join('')326 327 // Get current position.328 function now() {329 return {330 line,331 column,332 offset: index + ((point ? point.offset : 0) || 0)333 }334 }335 336 /**337 * Handle the warning.338 *339 * @param {1|2|3|4|5|6|7} code340 * @param {number} offset341 */342 function warning(code, offset) {343 /** @type {ReturnType<now>} */344 let position345 346 if (settings.warning) {347 position = now()348 position.column += offset349 position.offset += offset350 351 settings.warning.call(352 settings.warningContext || undefined,353 messages[code],354 position,355 code356 )357 }358 }359 360 /**361 * Flush `queue` (normal text).362 * Macro invoked before each reference and at the end of `value`.363 * Does nothing when `queue` is empty.364 */365 function flush() {366 if (queue) {367 result.push(queue)368 369 if (settings.text) {370 settings.text.call(settings.textContext || undefined, queue, {371 start: previous,372 end: now()373 })374 }375 376 queue = ''377 }378 }379}380 381/**382 * Check if `character` is outside the permissible unicode range.383 *384 * @param {number} code385 * @returns {boolean}386 */387function prohibited(code) {388 return (code >= 0xd800 && code <= 0xdfff) || code > 0x10ffff389}390 391/**392 * Check if `character` is disallowed.393 *394 * @param {number} code395 * @returns {boolean}396 */397function disallowed(code) {398 return (399 (code >= 0x0001 && code <= 0x0008) ||400 code === 0x000b ||401 (code >= 0x000d && code <= 0x001f) ||402 (code >= 0x007f && code <= 0x009f) ||403 (code >= 0xfdd0 && code <= 0xfdef) ||404 (code & 0xffff) === 0xffff ||405 (code & 0xffff) === 0xfffe406 )407}408 