index.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. /**
  2. * @import {Point} from 'unist'
  3. * @import {Options} from '../index.js'
  4. */
  5. import {characterEntitiesLegacy} from 'character-entities-legacy'
  6. import {characterReferenceInvalid} from 'character-reference-invalid'
  7. import {isDecimal} from 'is-decimal'
  8. import {isHexadecimal} from 'is-hexadecimal'
  9. import {isAlphanumerical} from 'is-alphanumerical'
  10. import {decodeNamedCharacterReference} from 'decode-named-character-reference'
  11. // Warning messages.
  12. const messages = [
  13. '',
  14. /* 1: Non terminated (named) */
  15. 'Named character references must be terminated by a semicolon',
  16. /* 2: Non terminated (numeric) */
  17. 'Numeric character references must be terminated by a semicolon',
  18. /* 3: Empty (named) */
  19. 'Named character references cannot be empty',
  20. /* 4: Empty (numeric) */
  21. 'Numeric character references cannot be empty',
  22. /* 5: Unknown (named) */
  23. 'Named character references must be known',
  24. /* 6: Disallowed (numeric) */
  25. 'Numeric character references cannot be disallowed',
  26. /* 7: Prohibited (numeric) */
  27. 'Numeric character references cannot be outside the permissible Unicode range'
  28. ]
  29. /**
  30. * Parse HTML character references.
  31. *
  32. * @param {string} value
  33. * @param {Readonly<Options> | null | undefined} [options]
  34. */
  35. export function parseEntities(value, options) {
  36. const settings = options || {}
  37. const additional =
  38. typeof settings.additional === 'string'
  39. ? settings.additional.charCodeAt(0)
  40. : settings.additional
  41. /** @type {Array<string>} */
  42. const result = []
  43. let index = 0
  44. let lines = -1
  45. let queue = ''
  46. /** @type {Point | undefined} */
  47. let point
  48. /** @type {Array<number>|undefined} */
  49. let indent
  50. if (settings.position) {
  51. if ('start' in settings.position || 'indent' in settings.position) {
  52. // @ts-expect-error: points don’t have indent.
  53. indent = settings.position.indent
  54. // @ts-expect-error: points don’t have indent.
  55. point = settings.position.start
  56. } else {
  57. point = settings.position
  58. }
  59. }
  60. let line = (point ? point.line : 0) || 1
  61. let column = (point ? point.column : 0) || 1
  62. // Cache the current point.
  63. let previous = now()
  64. /** @type {number|undefined} */
  65. let character
  66. // Ensure the algorithm walks over the first character (inclusive).
  67. index--
  68. while (++index <= value.length) {
  69. // If the previous character was a newline.
  70. if (character === 10 /* `\n` */) {
  71. column = (indent ? indent[lines] : 0) || 1
  72. }
  73. character = value.charCodeAt(index)
  74. if (character === 38 /* `&` */) {
  75. const following = value.charCodeAt(index + 1)
  76. // The behavior depends on the identity of the next character.
  77. if (
  78. following === 9 /* `\t` */ ||
  79. following === 10 /* `\n` */ ||
  80. following === 12 /* `\f` */ ||
  81. following === 32 /* ` ` */ ||
  82. following === 38 /* `&` */ ||
  83. following === 60 /* `<` */ ||
  84. Number.isNaN(following) ||
  85. (additional && following === additional)
  86. ) {
  87. // Not a character reference.
  88. // No characters are consumed, and nothing is returned.
  89. // This is not an error, either.
  90. queue += String.fromCharCode(character)
  91. column++
  92. continue
  93. }
  94. const start = index + 1
  95. let begin = start
  96. let end = start
  97. /** @type {string} */
  98. let type
  99. if (following === 35 /* `#` */) {
  100. // Numerical reference.
  101. end = ++begin
  102. // The behavior further depends on the next character.
  103. const following = value.charCodeAt(end)
  104. if (following === 88 /* `X` */ || following === 120 /* `x` */) {
  105. // ASCII hexadecimal digits.
  106. type = 'hexadecimal'
  107. end = ++begin
  108. } else {
  109. // ASCII decimal digits.
  110. type = 'decimal'
  111. }
  112. } else {
  113. // Named reference.
  114. type = 'named'
  115. }
  116. let characterReferenceCharacters = ''
  117. let characterReference = ''
  118. let characters = ''
  119. // Each type of character reference accepts different characters.
  120. // This test is used to detect whether a reference has ended (as the semicolon
  121. // is not strictly needed).
  122. const test =
  123. type === 'named'
  124. ? isAlphanumerical
  125. : type === 'decimal'
  126. ? isDecimal
  127. : isHexadecimal
  128. end--
  129. while (++end <= value.length) {
  130. const following = value.charCodeAt(end)
  131. if (!test(following)) {
  132. break
  133. }
  134. characters += String.fromCharCode(following)
  135. // Check if we can match a legacy named reference.
  136. // If so, we cache that as the last viable named reference.
  137. // This ensures we do not need to walk backwards later.
  138. if (type === 'named' && characterEntitiesLegacy.includes(characters)) {
  139. characterReferenceCharacters = characters
  140. // @ts-expect-error: always able to decode.
  141. characterReference = decodeNamedCharacterReference(characters)
  142. }
  143. }
  144. let terminated = value.charCodeAt(end) === 59 /* `;` */
  145. if (terminated) {
  146. end++
  147. const namedReference =
  148. type === 'named' ? decodeNamedCharacterReference(characters) : false
  149. if (namedReference) {
  150. characterReferenceCharacters = characters
  151. characterReference = namedReference
  152. }
  153. }
  154. let diff = 1 + end - start
  155. let reference = ''
  156. if (!terminated && settings.nonTerminated === false) {
  157. // Empty.
  158. } else if (!characters) {
  159. // An empty (possible) reference is valid, unless it’s numeric (thus an
  160. // ampersand followed by an octothorp).
  161. if (type !== 'named') {
  162. warning(4 /* Empty (numeric) */, diff)
  163. }
  164. } else if (type === 'named') {
  165. // An ampersand followed by anything unknown, and not terminated, is
  166. // invalid.
  167. if (terminated && !characterReference) {
  168. warning(5 /* Unknown (named) */, 1)
  169. } else {
  170. // If there’s something after an named reference which is not known,
  171. // cap the reference.
  172. if (characterReferenceCharacters !== characters) {
  173. end = begin + characterReferenceCharacters.length
  174. diff = 1 + end - begin
  175. terminated = false
  176. }
  177. // If the reference is not terminated, warn.
  178. if (!terminated) {
  179. const reason = characterReferenceCharacters
  180. ? 1 /* Non terminated (named) */
  181. : 3 /* Empty (named) */
  182. if (settings.attribute) {
  183. const following = value.charCodeAt(end)
  184. if (following === 61 /* `=` */) {
  185. warning(reason, diff)
  186. characterReference = ''
  187. } else if (isAlphanumerical(following)) {
  188. characterReference = ''
  189. } else {
  190. warning(reason, diff)
  191. }
  192. } else {
  193. warning(reason, diff)
  194. }
  195. }
  196. }
  197. reference = characterReference
  198. } else {
  199. if (!terminated) {
  200. // All nonterminated numeric references are not rendered, and emit a
  201. // warning.
  202. warning(2 /* Non terminated (numeric) */, diff)
  203. }
  204. // When terminated and numerical, parse as either hexadecimal or
  205. // decimal.
  206. let referenceCode = Number.parseInt(
  207. characters,
  208. type === 'hexadecimal' ? 16 : 10
  209. )
  210. // Emit a warning when the parsed number is prohibited, and replace with
  211. // replacement character.
  212. if (prohibited(referenceCode)) {
  213. warning(7 /* Prohibited (numeric) */, diff)
  214. reference = String.fromCharCode(65533 /* `�` */)
  215. } else if (referenceCode in characterReferenceInvalid) {
  216. // Emit a warning when the parsed number is disallowed, and replace by
  217. // an alternative.
  218. warning(6 /* Disallowed (numeric) */, diff)
  219. reference = characterReferenceInvalid[referenceCode]
  220. } else {
  221. // Parse the number.
  222. let output = ''
  223. // Emit a warning when the parsed number should not be used.
  224. if (disallowed(referenceCode)) {
  225. warning(6 /* Disallowed (numeric) */, diff)
  226. }
  227. // Serialize the number.
  228. if (referenceCode > 0xffff) {
  229. referenceCode -= 0x10000
  230. output += String.fromCharCode(
  231. (referenceCode >>> (10 & 0x3ff)) | 0xd800
  232. )
  233. referenceCode = 0xdc00 | (referenceCode & 0x3ff)
  234. }
  235. reference = output + String.fromCharCode(referenceCode)
  236. }
  237. }
  238. // Found it!
  239. // First eat the queued characters as normal text, then eat a reference.
  240. if (reference) {
  241. flush()
  242. previous = now()
  243. index = end - 1
  244. column += end - start + 1
  245. result.push(reference)
  246. const next = now()
  247. next.offset++
  248. if (settings.reference) {
  249. settings.reference.call(
  250. settings.referenceContext || undefined,
  251. reference,
  252. {start: previous, end: next},
  253. value.slice(start - 1, end)
  254. )
  255. }
  256. previous = next
  257. } else {
  258. // If we could not find a reference, queue the checked characters (as
  259. // normal characters), and move the pointer to their end.
  260. // This is possible because we can be certain neither newlines nor
  261. // ampersands are included.
  262. characters = value.slice(start - 1, end)
  263. queue += characters
  264. column += characters.length
  265. index = end - 1
  266. }
  267. } else {
  268. // Handle anything other than an ampersand, including newlines and EOF.
  269. if (character === 10 /* `\n` */) {
  270. line++
  271. lines++
  272. column = 0
  273. }
  274. if (Number.isNaN(character)) {
  275. flush()
  276. } else {
  277. queue += String.fromCharCode(character)
  278. column++
  279. }
  280. }
  281. }
  282. // Return the reduced nodes.
  283. return result.join('')
  284. // Get current position.
  285. function now() {
  286. return {
  287. line,
  288. column,
  289. offset: index + ((point ? point.offset : 0) || 0)
  290. }
  291. }
  292. /**
  293. * Handle the warning.
  294. *
  295. * @param {1|2|3|4|5|6|7} code
  296. * @param {number} offset
  297. */
  298. function warning(code, offset) {
  299. /** @type {ReturnType<now>} */
  300. let position
  301. if (settings.warning) {
  302. position = now()
  303. position.column += offset
  304. position.offset += offset
  305. settings.warning.call(
  306. settings.warningContext || undefined,
  307. messages[code],
  308. position,
  309. code
  310. )
  311. }
  312. }
  313. /**
  314. * Flush `queue` (normal text).
  315. * Macro invoked before each reference and at the end of `value`.
  316. * Does nothing when `queue` is empty.
  317. */
  318. function flush() {
  319. if (queue) {
  320. result.push(queue)
  321. if (settings.text) {
  322. settings.text.call(settings.textContext || undefined, queue, {
  323. start: previous,
  324. end: now()
  325. })
  326. }
  327. queue = ''
  328. }
  329. }
  330. }
  331. /**
  332. * Check if `character` is outside the permissible unicode range.
  333. *
  334. * @param {number} code
  335. * @returns {boolean}
  336. */
  337. function prohibited(code) {
  338. return (code >= 0xd800 && code <= 0xdfff) || code > 0x10ffff
  339. }
  340. /**
  341. * Check if `character` is disallowed.
  342. *
  343. * @param {number} code
  344. * @returns {boolean}
  345. */
  346. function disallowed(code) {
  347. return (
  348. (code >= 0x0001 && code <= 0x0008) ||
  349. code === 0x000b ||
  350. (code >= 0x000d && code <= 0x001f) ||
  351. (code >= 0x007f && code <= 0x009f) ||
  352. (code >= 0xfdd0 && code <= 0xfdef) ||
  353. (code & 0xffff) === 0xffff ||
  354. (code & 0xffff) === 0xfffe
  355. )
  356. }