| [9af201e] | 1 | import {bodyRegExps, namedReferences} from './named-references.js';
|
|---|
| 2 | import {numericUnicodeMap} from './numeric-unicode-map.js';
|
|---|
| 3 | import {fromCodePoint, getCodePoint} from './surrogate-pairs.js';
|
|---|
| 4 |
|
|---|
| 5 | const allNamedReferences = {
|
|---|
| 6 | ...namedReferences,
|
|---|
| 7 | all: namedReferences.html5
|
|---|
| 8 | };
|
|---|
| 9 |
|
|---|
| 10 | export type Level = 'xml' | 'html4' | 'html5' | 'all';
|
|---|
| 11 |
|
|---|
| 12 | interface CommonOptions {
|
|---|
| 13 | level?: Level;
|
|---|
| 14 | }
|
|---|
| 15 |
|
|---|
| 16 | export type EncodeMode = 'specialChars' | 'nonAscii' | 'nonAsciiPrintable' | 'nonAsciiPrintableOnly' | 'extensive';
|
|---|
| 17 |
|
|---|
| 18 | export interface EncodeOptions extends CommonOptions {
|
|---|
| 19 | mode?: EncodeMode;
|
|---|
| 20 | numeric?: 'decimal' | 'hexadecimal';
|
|---|
| 21 | }
|
|---|
| 22 |
|
|---|
| 23 | export type DecodeScope = 'strict' | 'body' | 'attribute';
|
|---|
| 24 |
|
|---|
| 25 | export interface DecodeOptions extends CommonOptions {
|
|---|
| 26 | scope?: DecodeScope;
|
|---|
| 27 | }
|
|---|
| 28 |
|
|---|
| 29 | const encodeRegExps: Record<EncodeMode, RegExp> = {
|
|---|
| 30 | specialChars: /[<>'"&]/g,
|
|---|
| 31 | nonAscii: /[<>'"&\u0080-\uD7FF\uE000-\uFFFF\uDC00-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]?/g,
|
|---|
| 32 | nonAsciiPrintable: /[<>'"&\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF\uDC00-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]?/g,
|
|---|
| 33 | nonAsciiPrintableOnly: /[\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF\uDC00-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]?/g,
|
|---|
| 34 | extensive: /[\x01-\x0c\x0e-\x1f\x21-\x2c\x2e-\x2f\x3a-\x40\x5b-\x60\x7b-\x7d\x7f-\uD7FF\uE000-\uFFFF\uDC00-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]?/g
|
|---|
| 35 | };
|
|---|
| 36 |
|
|---|
| 37 | const defaultEncodeOptions: EncodeOptions = {
|
|---|
| 38 | mode: 'specialChars',
|
|---|
| 39 | level: 'all',
|
|---|
| 40 | numeric: 'decimal'
|
|---|
| 41 | };
|
|---|
| 42 |
|
|---|
| 43 | /** Encodes all the necessary (specified by `level`) characters in the text */
|
|---|
| 44 | export function encode(
|
|---|
| 45 | text: string | undefined | null,
|
|---|
| 46 | {mode = 'specialChars', numeric = 'decimal', level = 'all'}: EncodeOptions = defaultEncodeOptions
|
|---|
| 47 | ) {
|
|---|
| 48 | if (!text) {
|
|---|
| 49 | return '';
|
|---|
| 50 | }
|
|---|
| 51 |
|
|---|
| 52 | const encodeRegExp = encodeRegExps[mode];
|
|---|
| 53 | const references = allNamedReferences[level].characters;
|
|---|
| 54 | const isHex = numeric === 'hexadecimal';
|
|---|
| 55 |
|
|---|
| 56 | return String.prototype.replace.call(text, encodeRegExp, (input) => {
|
|---|
| 57 | let result = references[input];
|
|---|
| 58 | if (!result) {
|
|---|
| 59 | const code = input.length > 1 ? getCodePoint(input, 0)! : input.charCodeAt(0);
|
|---|
| 60 | result = (isHex ? '&#x' + code.toString(16) : '&#' + code) + ';';
|
|---|
| 61 | }
|
|---|
| 62 | return result;
|
|---|
| 63 | });
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | const defaultDecodeOptions: DecodeOptions = {
|
|---|
| 67 | scope: 'body',
|
|---|
| 68 | level: 'all'
|
|---|
| 69 | };
|
|---|
| 70 |
|
|---|
| 71 | const strict = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+);/g;
|
|---|
| 72 | const attribute = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+)[;=]?/g;
|
|---|
| 73 |
|
|---|
| 74 | const baseDecodeRegExps: Record<Exclude<Level, 'all'>, Record<DecodeScope, RegExp>> = {
|
|---|
| 75 | xml: {
|
|---|
| 76 | strict,
|
|---|
| 77 | attribute,
|
|---|
| 78 | body: bodyRegExps.xml
|
|---|
| 79 | },
|
|---|
| 80 | html4: {
|
|---|
| 81 | strict,
|
|---|
| 82 | attribute,
|
|---|
| 83 | body: bodyRegExps.html4
|
|---|
| 84 | },
|
|---|
| 85 | html5: {
|
|---|
| 86 | strict,
|
|---|
| 87 | attribute,
|
|---|
| 88 | body: bodyRegExps.html5
|
|---|
| 89 | }
|
|---|
| 90 | };
|
|---|
| 91 |
|
|---|
| 92 | const decodeRegExps: Record<Level, Record<DecodeScope, RegExp>> = {
|
|---|
| 93 | ...baseDecodeRegExps,
|
|---|
| 94 | all: baseDecodeRegExps.html5
|
|---|
| 95 | };
|
|---|
| 96 |
|
|---|
| 97 | const fromCharCode = String.fromCharCode;
|
|---|
| 98 | const outOfBoundsChar = fromCharCode(65533);
|
|---|
| 99 |
|
|---|
| 100 | const defaultDecodeEntityOptions: CommonOptions = {
|
|---|
| 101 | level: 'all'
|
|---|
| 102 | };
|
|---|
| 103 |
|
|---|
| 104 | function getDecodedEntity(
|
|---|
| 105 | entity: string,
|
|---|
| 106 | references: Record<string, string>,
|
|---|
| 107 | isAttribute: boolean,
|
|---|
| 108 | isStrict: boolean
|
|---|
| 109 | ): string {
|
|---|
| 110 | let decodeResult = entity;
|
|---|
| 111 | const decodeEntityLastChar = entity[entity.length - 1];
|
|---|
| 112 | if (isAttribute && decodeEntityLastChar === '=') {
|
|---|
| 113 | decodeResult = entity;
|
|---|
| 114 | } else if (isStrict && decodeEntityLastChar !== ';') {
|
|---|
| 115 | decodeResult = entity;
|
|---|
| 116 | } else {
|
|---|
| 117 | const decodeResultByReference = references[entity];
|
|---|
| 118 | if (decodeResultByReference) {
|
|---|
| 119 | decodeResult = decodeResultByReference;
|
|---|
| 120 | } else if (entity[0] === '&' && entity[1] === '#') {
|
|---|
| 121 | const decodeSecondChar = entity[2];
|
|---|
| 122 | const decodeCode =
|
|---|
| 123 | decodeSecondChar == 'x' || decodeSecondChar == 'X'
|
|---|
| 124 | ? parseInt(entity.substr(3), 16)
|
|---|
| 125 | : parseInt(entity.substr(2));
|
|---|
| 126 |
|
|---|
| 127 | decodeResult =
|
|---|
| 128 | decodeCode >= 0x10ffff
|
|---|
| 129 | ? outOfBoundsChar
|
|---|
| 130 | : decodeCode > 65535
|
|---|
| 131 | ? fromCodePoint(decodeCode)
|
|---|
| 132 | : fromCharCode(numericUnicodeMap[decodeCode] || decodeCode);
|
|---|
| 133 | }
|
|---|
| 134 | }
|
|---|
| 135 | return decodeResult;
|
|---|
| 136 | }
|
|---|
| 137 |
|
|---|
| 138 | /** Decodes a single entity */
|
|---|
| 139 | export function decodeEntity(
|
|---|
| 140 | entity: string | undefined | null,
|
|---|
| 141 | {level = 'all'}: CommonOptions = defaultDecodeEntityOptions
|
|---|
| 142 | ): string {
|
|---|
| 143 | if (!entity) {
|
|---|
| 144 | return '';
|
|---|
| 145 | }
|
|---|
| 146 | return getDecodedEntity(entity, allNamedReferences[level].entities, false, false);
|
|---|
| 147 | }
|
|---|
| 148 |
|
|---|
| 149 | /** Decodes all entities in the text */
|
|---|
| 150 | export function decode(
|
|---|
| 151 | text: string | undefined | null,
|
|---|
| 152 | {level = 'all', scope = level === 'xml' ? 'strict' : 'body'}: DecodeOptions = defaultDecodeOptions
|
|---|
| 153 | ) {
|
|---|
| 154 | if (!text) {
|
|---|
| 155 | return '';
|
|---|
| 156 | }
|
|---|
| 157 |
|
|---|
| 158 | const decodeRegExp = decodeRegExps[level][scope];
|
|---|
| 159 | const references = allNamedReferences[level].entities;
|
|---|
| 160 | const isAttribute = scope === 'attribute';
|
|---|
| 161 | const isStrict = scope === 'strict';
|
|---|
| 162 |
|
|---|
| 163 | return text.replace(decodeRegExp, (entity) => getDecodedEntity(entity, references, isAttribute, isStrict));
|
|---|
| 164 | }
|
|---|